Chris McDonough
2013-08-30 17dbdbfc87e0d13d9db6d6b94885ad1402607516
Merge branch 'master' of github.com:Pylons/pyramid
13 files modified
521 ■■■■■ changed files
CHANGES.txt 39 ●●●●● patch | view | raw | blame | history
docs/narr/hybrid.rst 102 ●●●●● patch | view | raw | blame | history
pyramid/httpexceptions.py 56 ●●●●● patch | view | raw | blame | history
pyramid/interfaces.py 15 ●●●● patch | view | raw | blame | history
pyramid/tests/pkgs/exceptionviewapp/__init__.py 8 ●●●●● patch | view | raw | blame | history
pyramid/tests/pkgs/exceptionviewapp/views.py 7 ●●●●● patch | view | raw | blame | history
pyramid/tests/test_httpexceptions.py 6 ●●●● patch | view | raw | blame | history
pyramid/tests/test_integration.py 4 ●●●● patch | view | raw | blame | history
pyramid/tests/test_traversal.py 27 ●●●●● patch | view | raw | blame | history
pyramid/tests/test_url.py 152 ●●●●● patch | view | raw | blame | history
pyramid/traversal.py 16 ●●●● patch | view | raw | blame | history
pyramid/url.py 87 ●●●●● patch | view | raw | blame | history
pyramid/urldispatch.py 2 ●●● patch | view | raw | blame | history
CHANGES.txt
@@ -4,6 +4,19 @@
Features
--------
- A new http exception subclass named ``pyramid.httpexceptions.HTTPSuccessful``
  was added.  You can use this class as the ``context`` of an exception
  view to catch all 200-series "exceptions" (e.g. "raise HTTPOk").  This
  also allows you to catch *only* the ``HTTPOk`` exception itself; previously
  this was impossible because a number of other exceptions
  (such as ``HTTPNoContent``) inherited from ``HTTPOk``, but now they do not.
- You can now generate "hybrid" urldispatch/traversal URLs more easily
  by using the new ``route_name``, ``route_kw`` and ``route_remainder_name``
  arguments to  ``request.resource_url`` and ``request.resource_path``.  See
  the new section of the "Combining Traversal and URL Dispatch" documentation
  chapter entitled  "Hybrid URL Generation".
- It is now possible to escape double braces in Pyramid scaffolds (unescaped, 
  these represent replacement values).  You can use ``\{\{a\}\}`` to
  represent a "bare" ``{{a}}``.  See 
@@ -151,10 +164,16 @@
- The ``pyramid.config.Configurator.add_route`` method now supports being
  called with an external URL as pattern. See
  https://github.com/Pylons/pyramid/issues/611 for more information.
  https://github.com/Pylons/pyramid/issues/611 and the documentation section
  in the "URL Dispatch" chapter entitled "External Routes" for more information.
Bug Fixes
---------
- It was not possible to use ``pyramid.httpexceptions.HTTPException`` as
  the ``context`` of an exception view as very general catchall for
  http-related exceptions when you wanted that exception view to override the
  default exception view.  See https://github.com/Pylons/pyramid/issues/985
- When the ``pyramid.reload_templates`` setting was true, and a Chameleon 
  template was reloaded, and the renderer specification named a macro 
@@ -231,6 +250,16 @@
  are now "reified" properties that look up a locale name and localizer
  respectively using the machinery described in the "Internationalization"
  chapter of the documentation.
- If you send an ``X-Vhm-Root`` header with a value that ends with a slash (or
  any number of slashes), the trailing slash(es) will be removed before a URL
  is generated when you use use ``request.resource_url`` or
  ``request.resource_path``.  Previously the virtual root path would not have
  trailing slashes stripped, which would influence URL generation.
- The ``pyramid.interfaces.IResourceURL`` interface has now grown two new
  attributes: ``virtual_path_tuple`` and ``physical_path_tuple``.  These should
  be the tuple form of the resource's path (physical and virtual).
1.4 (2012-12-18)
================
@@ -901,6 +930,14 @@
  finished callbacks are executed.  This is in support of the
  ``request.invoke_subrequest`` feature.
- The 200-series exception responses named ``HTTPCreated``, ``HTTPAccepted``,
  ``HTTPNonAuthoritativeInformation``, ``HTTPNoContent``, ``HTTPResetContent``,
  and ``HTTPPartialContent`` in ``pyramid.httpexceptions`` no longer inherit
  from ``HTTPOk``.  Instead they inherit from a new base class named
  ``HTTPSuccessful``.  This will have no effect on you unless you've registered
  an exception view for ``HTTPOk`` and expect that exception view to
  catch all the aforementioned exceptions.
Documentation
-------------
docs/narr/hybrid.rst
@@ -549,3 +549,105 @@
no object contained by the root object with the key ``bazbuz``. A
different request URI, such as ``/abc/foo/bar``, would invoke the
default ``myproject.views.abc`` view.
.. index::
   pair: hybrid urls; generating
.. _generating_hybrid_urls:
Generating Hybrid URLs
----------------------
.. versionadded:: 1.5
The :meth:`pyramid.request.Request.resource_url` method and the
:meth:`pyramid.request.Request.resource_path` method both accept optional
keyword arguments that make it easier to generate route-prefixed URLs that
contain paths to traversal resources:``route_name``, ``route_kw``, and
``route_remainder_name``.
Any route that has a pattern that contains a ``*remainder`` pattern (any
stararg remainder pattern, such as ``*traverse`` or ``*subpath`` or ``*fred``)
can be used as the target name for ``request.resource_url(..., route_name=)``
and ``request.resource_path(..., route_name=)``.
For example, let's imagine you have a route defined in your Pyramid application
like so:
.. code-block:: python
   config.add_route('mysection', '/mysection*traverse')
If you'd like to generate the URL ``http://example.com/mysection/a/``, you can
use the following incantation, assuming that the variable ``a`` below points to
a resource that is a child of the root with a ``__name__`` of ``a``:
.. code-block:: python
   request.resource_url(a, route_name='mysection')
You can generate only the path portion ``/mysection/a/`` assuming the same:
.. code-block:: python
   request.resource_path(a, route_name='mysection')
The path is virtual host aware, so if the ``X-Vhm-Root`` environ variable is
present in the request, and it's set to ``/a``, the above call to
``request.resource_url`` would generate ``http://example.com/mysection/``
and the above call to ``request.resource_path`` would generate ``/mysection/``.
See :ref:`virtual_root_support` for more information.
If the route you're trying to use needs simple dynamic part values to be filled
in to succesfully generate the URL, you can pass these as the ``route_kw``
argument to ``resource_url`` and ``resource_path``.  For example, assuming that
the route definition is like so:
.. code-block:: python
   config.add_route('mysection', '/{id}/mysection*traverse')
You can pass ``route_kw`` in to fill in ``{id}`` above:
.. code-block:: python
   request.resource_url(a, route_name='mysection', route_kw={'id':'1'})
If you pass ``route_kw`` but do not pass ``route_name``, ``route_kw`` will
be ignored.
By default this feature works by calling ``route_url`` under the hood,
and passing the value of the resource path to that function as ``traverse``.
If your route has a different ``*stararg`` remainder name (such as
``*subpath``), you can tell ``resource_url`` or ``resource_path`` to use that
instead of  ``traverse`` by passing ``route_remainder_name``.  For example,
if you have the following route:
.. code-block:: python
   config.add_route('mysection', '/mysection*subpath')
You can fill in the ``*subpath`` value using ``resource_url`` by doing:
.. code-block:: python
   request.resource_path(a, route_name='mysection',
                         route_remainder_name='subpath')
If you pass ``route_remainder_name`` but do not pass ``route_name``,
``route_remainder_name`` will be ignored.
If you try to use ``resource_path`` or ``resource_url`` when the ``route_name``
argument points at a route that does not have a remainder stararg, an error
will not be raised, but the generated URL will not contain any remainder
information either.
All other values that are normally passable to ``resource_path`` and
``resource_url`` (such as ``query``, ``anchor``, ``host``, ``port``, and
positional elements) work as you might expect in this configuration.
Note that this feature is incompatible with the ``__resource_url__`` feature
(see :ref:`overriding_resource_url_generation`) implemented on resource
objects.  Any  ``__resource_url__`` supplied by your resource will be ignored
when you pass ``route_name``.
pyramid/httpexceptions.py
@@ -13,7 +13,7 @@
Exception
  HTTPException
    HTTPOk
    HTTPSuccessful
      * 200 - HTTPOk
      * 201 - HTTPCreated
      * 202 - HTTPAccepted
@@ -149,11 +149,8 @@
            value = text_type(value)
    return value
class HTTPException(Exception): # bw compat
    """ Base class for all :term:`exception response` objects."""
@implementer(IExceptionResponse)
class WSGIHTTPException(Response, HTTPException):
class HTTPException(Response, Exception):
    ## You should set in subclasses:
    # code = 200
@@ -253,7 +250,7 @@
                'html_comment':html_comment,
                }
            body_tmpl = self.body_template_obj
            if WSGIHTTPException.body_template_obj is not body_tmpl:
            if HTTPException.body_template_obj is not body_tmpl:
                # Custom template; add headers to args
                for k, v in environ.items():
                    if (not k.startswith('wsgi.')) and ('.' in k):
@@ -289,7 +286,9 @@
        self.prepare(environ)
        return Response.__call__(self, environ, start_response)
class HTTPError(WSGIHTTPException):
WSGIHTTPException = HTTPException # b/c post 1.5
class HTTPError(HTTPException):
    """
    base class for exceptions with status codes in the 400s and 500s
@@ -297,7 +296,7 @@
    and that any work in progress should not be committed.  
    """
class HTTPRedirection(WSGIHTTPException):
class HTTPRedirection(HTTPException):
    """
    base class for exceptions with status codes in the 300s (redirections)
@@ -307,23 +306,30 @@
    condition.
    """
class HTTPOk(WSGIHTTPException):
class HTTPSuccessful(HTTPException):
    """
    Base class for exceptions with status codes in the 200s (successful
    responses)
    """
############################################################
## 2xx success
############################################################
class HTTPOk(HTTPSuccessful):
    """
    subclass of :class:`~HTTPSuccessful`
    Indicates that the request has succeeded.
    
    code: 200, title: OK
    """
    code = 200
    title = 'OK'
############################################################
## 2xx success
############################################################
class HTTPCreated(HTTPOk):
class HTTPCreated(HTTPSuccessful):
    """
    subclass of :class:`~HTTPOk`
    subclass of :class:`~HTTPSuccessful`
    This indicates that request has been fulfilled and resulted in a new
    resource being created.
@@ -333,9 +339,9 @@
    code = 201
    title = 'Created'
class HTTPAccepted(HTTPOk):
class HTTPAccepted(HTTPSuccessful):
    """
    subclass of :class:`~HTTPOk`
    subclass of :class:`~HTTPSuccessful`
    This indicates that the request has been accepted for processing, but the
    processing has not been completed.
@@ -346,9 +352,9 @@
    title = 'Accepted'
    explanation = 'The request is accepted for processing.'
class HTTPNonAuthoritativeInformation(HTTPOk):
class HTTPNonAuthoritativeInformation(HTTPSuccessful):
    """
    subclass of :class:`~HTTPOk`
    subclass of :class:`~HTTPSuccessful`
    This indicates that the returned metainformation in the entity-header is
    not the definitive set as available from the origin server, but is
@@ -359,9 +365,9 @@
    code = 203
    title = 'Non-Authoritative Information'
class HTTPNoContent(HTTPOk):
class HTTPNoContent(HTTPSuccessful):
    """
    subclass of :class:`~HTTPOk`
    subclass of :class:`~HTTPSuccessful`
    This indicates that the server has fulfilled the request but does
    not need to return an entity-body, and might want to return updated
@@ -373,9 +379,9 @@
    title = 'No Content'
    empty_body = True
class HTTPResetContent(HTTPOk):
class HTTPResetContent(HTTPSuccessful):
    """
    subclass of :class:`~HTTPOk`
    subclass of :class:`~HTTPSuccessful`
    This indicates that the server has fulfilled the request and
    the user agent SHOULD reset the document view which caused the
@@ -387,9 +393,9 @@
    title = 'Reset Content'
    empty_body = True
class HTTPPartialContent(HTTPOk):
class HTTPPartialContent(HTTPSuccessful):
    """
    subclass of :class:`~HTTPOk`
    subclass of :class:`~HTTPSuccessful`
    This indicates that the server has fulfilled the partial GET
    request for the resource.
pyramid/interfaces.py
@@ -692,6 +692,7 @@
    pregenerator = Attribute('This attribute should either be ``None`` or '
                             'a callable object implementing the '
                             '``IRoutePregenerator`` interface')
    def match(path):
        """
        If the ``path`` passed to this function can be matched by the
@@ -738,8 +739,18 @@
        matched.  Static routes will not be considered for matching.  """
class IResourceURL(Interface):
    virtual_path = Attribute('The virtual url path of the resource.')
    physical_path = Attribute('The physical url path of the resource.')
    virtual_path = Attribute(
        'The virtual url path of the resource as a string.'
        )
    physical_path = Attribute(
        'The physical url path of the resource as a string.'
        )
    virtual_path_tuple = Attribute(
        'The virtual url path of the resource as a tuple.  (New in 1.5)'
        )
    physical_path_tuple = Attribute(
        'The physical url path of the resource as a tuple. (New in 1.5)'
        )
class IContextURL(IResourceURL):
    """ An adapter which deals with URLs related to a context.
pyramid/tests/pkgs/exceptionviewapp/__init__.py
@@ -1,5 +1,8 @@
from pyramid.httpexceptions import HTTPException
def includeme(config):
    config.add_route('route_raise_exception', 'route_raise_exception')
    config.add_route('route_raise_httpexception', 'route_raise_httpexception')
    config.add_route('route_raise_exception2', 'route_raise_exception2',
                     factory='.models.route_factory')
    config.add_route('route_raise_exception3', 'route_raise_exception3',
@@ -21,3 +24,8 @@
                    route_name='route_raise_exception4')
    config.add_view('.views.whoa', context='.models.AnException',
                    route_name='route_raise_exception4')
    config.add_view('.views.raise_httpexception',
                    route_name='route_raise_httpexception')
    config.add_view('.views.catch_httpexception', context=HTTPException)
pyramid/tests/pkgs/exceptionviewapp/views.py
@@ -1,5 +1,6 @@
from webob import Response
from .models import AnException
from pyramid.httpexceptions import HTTPBadRequest
def no(request):
    return Response('no')
@@ -15,3 +16,9 @@
def raise_exception(request):
    raise AnException()
def raise_httpexception(request):
    raise HTTPBadRequest
def catch_httpexception(request):
    return Response('caught')
pyramid/tests/test_httpexceptions.py
@@ -57,10 +57,10 @@
        duo = DummyUnicodeObject()
        self.assertEqual(self._callFUT(duo), text_('42'))
class TestWSGIHTTPException(unittest.TestCase):
class TestHTTPException(unittest.TestCase):
    def _getTargetClass(self):
        from pyramid.httpexceptions import WSGIHTTPException
        return WSGIHTTPException
        from pyramid.httpexceptions import HTTPException
        return HTTPException
    def _getTargetSubclass(self, code='200', title='OK',
                           explanation='explanation', empty_body=False):
pyramid/tests/test_integration.py
@@ -465,6 +465,10 @@
        res = self.testapp.get('/route_raise_exception4', status=200)
        self.assertTrue(b'whoa' in res.body)
    def test_raise_httpexception(self):
        res = self.testapp.get('/route_raise_httpexception', status=200)
        self.assertTrue(b'caught' in res.body)
class TestConflictApp(unittest.TestCase):
    package = 'pyramid.tests.pkgs.conflictapp'
    def _makeConfig(self):
pyramid/tests/test_traversal.py
@@ -1063,7 +1063,28 @@
        context_url = self._makeOne(two, request)
        self.assertEqual(context_url.physical_path, '/one/two/')
        self.assertEqual(context_url.virtual_path, '/two/')
        self.assertEqual(context_url.physical_path_tuple, ('', 'one', 'two',''))
        self.assertEqual(context_url.virtual_path_tuple, ('', 'two', ''))
    def test_IResourceURL_attributes_vroot_ends_with_slash(self):
        from pyramid.interfaces import VH_ROOT_KEY
        root = DummyContext()
        root.__parent__ = None
        root.__name__ = None
        one = DummyContext()
        one.__parent__ = root
        one.__name__ = 'one'
        two = DummyContext()
        two.__parent__ = one
        two.__name__ = 'two'
        environ = {VH_ROOT_KEY:'/one/'}
        request = DummyRequest(environ)
        context_url = self._makeOne(two, request)
        self.assertEqual(context_url.physical_path, '/one/two/')
        self.assertEqual(context_url.virtual_path, '/two/')
        self.assertEqual(context_url.physical_path_tuple, ('', 'one', 'two',''))
        self.assertEqual(context_url.virtual_path_tuple, ('', 'two', ''))
    def test_IResourceURL_attributes_no_vroot(self):
        root = DummyContext()
        root.__parent__ = None
@@ -1079,7 +1100,9 @@
        context_url = self._makeOne(two, request)
        self.assertEqual(context_url.physical_path, '/one/two/')
        self.assertEqual(context_url.virtual_path, '/one/two/')
        self.assertEqual(context_url.physical_path_tuple, ('', 'one', 'two',''))
        self.assertEqual(context_url.virtual_path_tuple, ('', 'one', 'two', ''))
class TestVirtualRoot(unittest.TestCase):
    def setUp(self):
        cleanUp()
pyramid/tests/test_url.py
@@ -46,11 +46,12 @@
        from pyramid.interfaces import IResourceURL
        from zope.interface import Interface
        class DummyResourceURL(object):
            def __init__(self, context, request):
                self.physical_path = '/context/'
                self.virtual_path = '/context/'
            physical_path = '/context/'
            virtual_path = '/context/'
            def __init__(self, context, request): pass
        reg.registerAdapter(DummyResourceURL, (Interface, Interface),
                            IResourceURL)
        return DummyResourceURL
    def test_resource_url_root_default(self):
        request = self._makeOne()
@@ -255,6 +256,148 @@
        root.__resource_url__ = resource_url
        result = request.resource_url(root)
        self.assertEqual(result, 'http://example.com/contextabc/')
    def test_resource_url_with_route_name_no_remainder_on_adapter(self):
        from pyramid.interfaces import IRoutesMapper
        environ = {
            'wsgi.url_scheme':'http',
            'SERVER_PORT':'8080',
            'SERVER_NAME':'example.com',
            }
        request = self._makeOne(environ)
        adapter = self._registerResourceURL(request.registry)
        # no virtual_path_tuple on adapter
        adapter.virtual_path = '/a/b/c/'
        route = DummyRoute('/1/2/3')
        mapper = DummyRoutesMapper(route)
        request.registry.registerUtility(mapper, IRoutesMapper)
        root = DummyContext()
        result = request.resource_url(root, route_name='foo')
        self.assertEqual(result, 'http://example.com:5432/1/2/3')
        self.assertEqual(route.kw, {'traverse': ('', 'a', 'b', 'c', '')})
    def test_resource_url_with_route_name_remainder_on_adapter(self):
        from pyramid.interfaces import IRoutesMapper
        environ = {
            'wsgi.url_scheme':'http',
            'SERVER_PORT':'8080',
            'SERVER_NAME':'example.com',
            }
        request = self._makeOne(environ)
        adapter = self._registerResourceURL(request.registry)
        # virtual_path_tuple on adapter
        adapter.virtual_path_tuple = ('', 'a', 'b', 'c', '')
        route = DummyRoute('/1/2/3')
        mapper = DummyRoutesMapper(route)
        request.registry.registerUtility(mapper, IRoutesMapper)
        root = DummyContext()
        result = request.resource_url(root, route_name='foo')
        self.assertEqual(result, 'http://example.com:5432/1/2/3')
        self.assertEqual(route.kw, {'traverse': ('', 'a', 'b', 'c', '')})
    def test_resource_url_with_route_name_and_app_url(self):
        from pyramid.interfaces import IRoutesMapper
        environ = {
            'wsgi.url_scheme':'http',
            'SERVER_PORT':'8080',
            'SERVER_NAME':'example.com',
            }
        request = self._makeOne(environ)
        adapter = self._registerResourceURL(request.registry)
        # virtual_path_tuple on adapter
        adapter.virtual_path_tuple = ('', 'a', 'b', 'c', '')
        route = DummyRoute('/1/2/3')
        mapper = DummyRoutesMapper(route)
        request.registry.registerUtility(mapper, IRoutesMapper)
        root = DummyContext()
        result = request.resource_url(root, route_name='foo', app_url='app_url')
        self.assertEqual(result, 'app_url/1/2/3')
        self.assertEqual(route.kw, {'traverse': ('', 'a', 'b', 'c', '')})
    def test_resource_url_with_route_name_and_scheme_host_port_etc(self):
        from pyramid.interfaces import IRoutesMapper
        environ = {
            'wsgi.url_scheme':'http',
            'SERVER_PORT':'8080',
            'SERVER_NAME':'example.com',
            }
        request = self._makeOne(environ)
        adapter = self._registerResourceURL(request.registry)
        # virtual_path_tuple on adapter
        adapter.virtual_path_tuple = ('', 'a', 'b', 'c', '')
        route = DummyRoute('/1/2/3')
        mapper = DummyRoutesMapper(route)
        request.registry.registerUtility(mapper, IRoutesMapper)
        root = DummyContext()
        result = request.resource_url(root, route_name='foo', scheme='scheme',
                                      host='host', port='port', query={'a':'1'},
                                      anchor='anchor')
        self.assertEqual(result, 'scheme://host:port/1/2/3?a=1#anchor')
        self.assertEqual(route.kw, {'traverse': ('', 'a', 'b', 'c', '')})
    def test_resource_url_with_route_name_and_route_kwargs(self):
        from pyramid.interfaces import IRoutesMapper
        environ = {
            'wsgi.url_scheme':'http',
            'SERVER_PORT':'8080',
            'SERVER_NAME':'example.com',
            }
        request = self._makeOne(environ)
        adapter = self._registerResourceURL(request.registry)
        # virtual_path_tuple on adapter
        adapter.virtual_path_tuple = ('', 'a', 'b', 'c', '')
        route = DummyRoute('/1/2/3')
        mapper = DummyRoutesMapper(route)
        request.registry.registerUtility(mapper, IRoutesMapper)
        root = DummyContext()
        result = request.resource_url(
            root, route_name='foo', route_kw={'a':'1', 'b':'2'})
        self.assertEqual(result, 'http://example.com:5432/1/2/3')
        self.assertEqual(
            route.kw,
            {'traverse': ('', 'a', 'b', 'c', ''),
             'a':'1',
             'b':'2'}
            )
    def test_resource_url_with_route_name_and_elements(self):
        from pyramid.interfaces import IRoutesMapper
        environ = {
            'wsgi.url_scheme':'http',
            'SERVER_PORT':'8080',
            'SERVER_NAME':'example.com',
            }
        request = self._makeOne(environ)
        adapter = self._registerResourceURL(request.registry)
        # virtual_path_tuple on adapter
        adapter.virtual_path_tuple = ('', 'a', 'b', 'c', '')
        route = DummyRoute('/1/2/3')
        mapper = DummyRoutesMapper(route)
        request.registry.registerUtility(mapper, IRoutesMapper)
        root = DummyContext()
        result = request.resource_url(root, 'e1', 'e2', route_name='foo')
        self.assertEqual(result,  'http://example.com:5432/1/2/3/e1/e2')
        self.assertEqual(route.kw, {'traverse': ('', 'a', 'b', 'c', '')})
    def test_resource_url_with_route_name_and_remainder_name(self):
        from pyramid.interfaces import IRoutesMapper
        environ = {
            'wsgi.url_scheme':'http',
            'SERVER_PORT':'8080',
            'SERVER_NAME':'example.com',
            }
        request = self._makeOne(environ)
        adapter = self._registerResourceURL(request.registry)
        # virtual_path_tuple on adapter
        adapter.virtual_path_tuple = ('', 'a', 'b', 'c', '')
        route = DummyRoute('/1/2/3')
        mapper = DummyRoutesMapper(route)
        request.registry.registerUtility(mapper, IRoutesMapper)
        root = DummyContext()
        result = request.resource_url(root, route_name='foo',
                                      route_remainder_name='fred')
        self.assertEqual(result, 'http://example.com:5432/1/2/3')
        self.assertEqual(route.kw, {'fred': ('', 'a', 'b', 'c', '')})
        
    def test_resource_path(self):
        request = self._makeOne()
@@ -503,7 +646,8 @@
        from pyramid.interfaces import IRoutesMapper
        from webob.multidict import GetDict
        request = self._makeOne()
        request.GET = GetDict([('q', '123'), ('b', '2'), ('b', '2'), ('q', '456')], {})
        request.GET = GetDict(
            [('q', '123'), ('b', '2'), ('b', '2'), ('q', '456')], {})
        route = DummyRoute('/1/2/3')
        mapper = DummyRoutesMapper(route=route)
        request.matched_route = route
pyramid/traversal.py
@@ -733,11 +733,15 @@
    vroot_varname = VH_ROOT_KEY
    def __init__(self, resource, request):
        physical_path = resource_path(resource)
        if physical_path != '/':
        physical_path_tuple = resource_path_tuple(resource)
        physical_path = _join_path_tuple(physical_path_tuple)
        if physical_path_tuple != ('',):
            physical_path_tuple = physical_path_tuple + ('',)
            physical_path = physical_path + '/'
        virtual_path = physical_path
        virtual_path_tuple = physical_path_tuple
        environ = request.environ
        vroot_path = environ.get(self.vroot_varname)
@@ -745,11 +749,17 @@
        # if the physical path starts with the virtual root path, trim it out
        # of the virtual path
        if vroot_path is not None:
            if physical_path.startswith(vroot_path):
            vroot_path = vroot_path.rstrip('/')
            if vroot_path and physical_path.startswith(vroot_path):
                vroot_path_tuple = tuple(vroot_path.split('/'))
                numels = len(vroot_path_tuple)
                virtual_path_tuple = ('',) + physical_path_tuple[numels:]
                virtual_path = physical_path[len(vroot_path):]
        self.virtual_path = virtual_path    # IResourceURL attr
        self.physical_path = physical_path  # IResourceURL attr
        self.virtual_path_tuple = virtual_path_tuple # IResourceURL attr (1.5)
        self.physical_path_tuple = physical_path_tuple # IResourceURL attr (1.5)
        # bw compat for IContextURL methods
        self.resource = resource
pyramid/url.py
@@ -401,10 +401,66 @@
        passed for ``scheme``, ``host``, and/or ``port`` will be ignored.
        If the ``resource`` passed in has a ``__resource_url__`` method, it
        will be used to generate the URL (scheme, host, port, path) that for
        the base resource which is operated upon by this function.  See also
        will be used to generate the URL (scheme, host, port, path) for the
        base resource which is operated upon by this function.  See also
        :ref:`overriding_resource_url_generation`.
        .. versionadded:: 1.5
           ``route_name``, ``route_kw``, and ``route_remainder_name``
        If ``route_name`` is passed, this function will delegate its URL
        production to the ``route_url`` function.  Calling
        ``resource_url(someresource, 'element1', 'element2', query={'a':1},
        route_name='blogentry')`` is roughly equivalent to doing::
           remainder_path = request.resource_path(someobject)
           url = request.route_url(
                     'blogentry',
                     'element1',
                     'element2',
                     _query={'a':'1'},
                     traverse=traversal_path,
                     )
        It is only sensible to pass ``route_name`` if the route being named has
        a ``*remainder`` stararg value such as ``*traverse``.  The remainder
        value will be ignored in the output otherwise.
        By default, the resource path value will be passed as the name
        ``traverse`` when ``route_url`` is called.  You can influence this by
        passing a different ``route_remainder_name`` value if the route has a
        different ``*stararg`` value at its end.  For example if the route
        pattern you want to replace has a ``*subpath`` stararg ala
        ``/foo*subpath``::
           request.resource_url(
                          resource,
                          route_name='myroute',
                          route_remainder_name='subpath'
                          )
        If ``route_name`` is passed, it is also permissible to pass
        ``route_kw``, which will passed as additional keyword arguments to
        ``route_url``.  Saying ``resource_url(someresource, 'element1',
        'element2', route_name='blogentry', route_kw={'id':'4'},
        _query={'a':'1'})`` is roughly equivalent to::
           remainder_path = request.resource_path_tuple(someobject)
           kw = {'id':'4', '_query':{'a':'1'}, 'traverse':traversal_path}
           url = request.route_url(
                     'blogentry',
                     'element1',
                     'element2',
                     **kw,
                     )
        If ``route_kw`` or ``route_remainder_name`` is passed, but
        ``route_name`` is not passed, both ``route_kw`` and
        ``route_remainder_name`` will be ignored.  If ``route_name``
        is passed, the ``__resource_url__`` method of the resource passed is
        ignored unconditionally.  This feature is incompatible with
        resources which generate their own URLs.
        .. note::
           If the :term:`resource` used is the result of a :term:`traversal`, it
@@ -452,12 +508,37 @@
            resource_url = url_adapter()
        else:
            # newer-style IResourceURL adapter (Pyramid 1.3 and after)
            # IResourceURL adapter (Pyramid 1.3 and after)
            app_url = None
            scheme = None
            host = None
            port = None
            if 'route_name' in kw:
                newkw = {}
                route_name = kw['route_name']
                remainder = getattr(url_adapter, 'virtual_path_tuple', None)
                if remainder is None:
                    # older user-supplied IResourceURL adapter without 1.5
                    # virtual_path_tuple
                    remainder = tuple(url_adapter.virtual_path.split('/'))
                remainder_name = kw.get('route_remainder_name', 'traverse')
                newkw[remainder_name] = remainder
                for name in (
                    'app_url', 'scheme', 'host', 'port', 'query', 'anchor'
                    ):
                    val = kw.get(name, None)
                    if val is not None:
                        newkw['_' + name] = val
                if 'route_kw' in kw:
                    route_kw = kw.get('route_kw')
                    if route_kw is not None:
                        newkw.update(route_kw)
                return self.route_url(route_name, *elements, **newkw)
            if 'app_url' in kw:
                app_url = kw['app_url']
pyramid/urldispatch.py
@@ -91,7 +91,7 @@
# stolen from bobo and modified
old_route_re = re.compile(r'(\:[_a-zA-Z]\w*)')
star_at_end = re.compile(r'\*\w*$')
star_at_end = re.compile(r'\*(\w*)$')
# The tortuous nature of the regex named ``route_re`` below is due to the
# fact that we need to support at least one level of "inner" squigglies