Chris McDonough
2013-08-30 db0185ff8516b852aad0a1bdb0cbcee63d28c4d2
first cut at hybrid url generation; still needs tests for resource_url logic
8 files modified
201 ■■■■■ changed files
CHANGES.txt 10 ●●●●● patch | view | raw | blame | history
pyramid/interfaces.py 24 ●●●●● patch | view | raw | blame | history
pyramid/tests/test_request.py 1 ●●●● patch | view | raw | blame | history
pyramid/tests/test_traversal.py 27 ●●●●● patch | view | raw | blame | history
pyramid/tests/test_url.py 29 ●●●●● patch | view | raw | blame | history
pyramid/traversal.py 16 ●●●● patch | view | raw | blame | history
pyramid/url.py 85 ●●●●● patch | view | raw | blame | history
pyramid/urldispatch.py 9 ●●●● patch | view | raw | blame | history
CHANGES.txt
@@ -232,6 +232,16 @@
  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)
================
pyramid/interfaces.py
@@ -692,6 +692,16 @@
    pregenerator = Attribute('This attribute should either be ``None`` or '
                             'a callable object implementing the '
                             '``IRoutePregenerator`` interface')
    remainder_name = Attribute(
        'The name of any stararg remainder that is present at the end of '
        'the pattern. For example, if the pattern is ``/foo*bar``, the '
        '``remainder_name`` will be ``bar``; if the pattern is ` '
        '`/foo*traverse``, the ``remainder_name`` will be ``traverse``. '
        'If the route does not have a stararg remainder name in its pattern, '
        'the value of ``remainder_name`` will be ``None``.  This attribute '
        'is new as of Pyramid 1.5.'
        )
    def match(path):
        """
        If the ``path`` passed to this function can be matched by the
@@ -738,8 +748,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 = 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/test_request.py
@@ -594,6 +594,7 @@
class DummyRoute:
    pregenerator = None
    remainder_name = None
    def __init__(self, result='/1/2/3'):
        self.result = result
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
@@ -441,6 +441,31 @@
        self.assertEqual(result,
                         'http://example2.com/1/2/3/element1?q=1#anchor')
    def test_route_url_with_remainder(self):
        from pyramid.interfaces import IRoutesMapper
        request = self._makeOne()
        route = DummyRoute('/1/2/3/')
        route.remainder_name = 'fred'
        mapper = DummyRoutesMapper(route=route)
        request.registry.registerUtility(mapper, IRoutesMapper)
        result = request.route_url('flub', _remainder='abc')
        self.assertEqual(result,
                         'http://example.com:5432/1/2/3/')
        self.assertEqual(route.kw['fred'], 'abc')
        self.assertFalse('_remainder' in route.kw)
    def test_route_url_with_remainder_name_already_in_kw(self):
        from pyramid.interfaces import IRoutesMapper
        request = self._makeOne()
        route = DummyRoute('/1/2/3/')
        route.remainder_name = 'fred'
        mapper = DummyRoutesMapper(route=route)
        request.registry.registerUtility(mapper, IRoutesMapper)
        self.assertRaises(
            ValueError,
            request.route_url, 'flub', _remainder='abc', fred='foo'
            )
    def test_route_url_integration_with_real_request(self):
        # to try to replicate https://github.com/Pylons/pyramid/issues/213
        from pyramid.interfaces import IRoutesMapper
@@ -503,7 +528,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
@@ -1113,6 +1139,7 @@
class DummyRoute:
    pregenerator = None
    name = 'route'
    remainder_name = None
    def __init__(self, result='/1/2/3'):
        self.result = result
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
@@ -192,6 +192,15 @@
        are passed, ``_app_url`` takes precedence and any values passed for
        ``_scheme``, ``_host``, and ``_port`` will be ignored.
        If a ``_remainder`` keyword argument is supplied, it will be used to
        replace *any* ``*remainder`` stararg at the end of the route pattern.
        For example, if the route pattern is ``/foo/*traverse``, and you pass
        ``_remainder=('a', 'b', 'c')``, it is entirely equivalent to passing
        ``traverse=('a', 'b', 'c')``, and in either case the generated path
        will be ``/foo/a/b/c``.  It is an error to pass both ``*remainder`` and
        the explicit value for a remainder name; a :exc:`ValueError` will be
        raised.  This feature was added in Pyramid 1.5.
        This function raises a :exc:`KeyError` if the URL cannot be
        generated due to missing replacement names.  Extra replacement
        names are ignored.
@@ -213,6 +222,7 @@
        if route.pregenerator is not None:
            elements, kw = route.pregenerator(self, elements, kw)
        remainder_name = route.remainder_name
        anchor = ''
        qs = ''
        app_url = None
@@ -247,6 +257,16 @@
                app_url = self._partial_application_url(scheme, host, port)
            else:
                app_url = self.application_url
        remainder = kw.pop('_remainder', None)
        if remainder and remainder_name:
            if remainder_name in kw:
                raise ValueError(
                    'Cannot pass both "%s" and "_remainder", '
                    'these conflict for this route' % remainder_name
                    )
            kw[remainder_name] = remainder
        path = route.generate(kw) # raises KeyError if generate fails
@@ -400,9 +420,48 @@
        are also passed, ``app_url`` will take precedence and the values
        passed for ``scheme``, ``host``, and/or ``port`` will be ignored.
        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'},
                     _remainder=remainder_path,
                     )
        It is only sensible to pass ``route_name`` if the route being named has
        a ``*remainder`` stararg value such as ``*traverse``.  The remainder
        will be ignored in the output otherwise.
        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 equivalent to::
           remainder_path = request.resource_path_tuple(someobject)
           kw = {'id':'4', '_query':{'a':'1'}, '_remainder':remainder_path}
           url = request.route_url(
                     'blogentry',
                     'element1',
                     'element2',
                     **kw,
                     )
        If route_kw is passed, but route_name is not passed, a
        :exc:`ValueError` will be raised.
        The ``route_name`` and ``route_kw`` arguments were added in Pyramid
        1.5.
        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`.
        .. note::
@@ -458,6 +517,28 @@
            host = None
            port = None
            if 'route_name' in kw:
                newkw = {}
                route_name = kw['route_name']
                remainder = getattr(resource_url, 'virtual_path_tuple', None)
                if remainder is None:
                    # older user-supplied IResourceURL adapter without 1.5
                    # virtual_path_tuple
                    remainder = tuple(resource_url.virtual_path.split('/'))
                newkw['_remainder'] = remainder
                for name in ('app_url', 'scheme', 'host', 'port'):
                    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
@@ -33,6 +33,7 @@
        self.pattern = pattern
        self.path = pattern # indefinite b/w compat, not in interface
        self.match, self.generate = _compile_route(pattern)
        self.remainder_name = get_remainder_name(pattern)
        self.name = name
        self.factory = factory
        self.predicates = predicates
@@ -91,7 +92,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
@@ -233,3 +234,9 @@
        return result
    return matcher, generator
def get_remainder_name(pattern):
    match = star_at_end.search(pattern)
    if match:
        return match.groups()[0]