Skip to content
Browse files

first cut at hybrid url generation; still needs tests for resource_ur…

…l logic
  • Loading branch information...
1 parent c0d5a5c commit db0185ff8516b852aad0a1bdb0cbcee63d28c4d2 @mcdonc mcdonc committed Aug 29, 2013
View
10 CHANGES.txt
@@ -232,6 +232,16 @@ Backwards Incompatibilities
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)
================
View
24 pyramid/interfaces.py
@@ -692,6 +692,16 @@ class IRoute(Interface):
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 @@ def __call__(request):
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.
View
1 pyramid/tests/test_request.py
@@ -594,6 +594,7 @@ def get_route(self, route_name):
class DummyRoute:
pregenerator = None
+ remainder_name = None
def __init__(self, result='/1/2/3'):
self.result = result
View
27 pyramid/tests/test_traversal.py
@@ -1063,7 +1063,28 @@ def test_IResourceURL_attributes_with_vroot(self):
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 @@ def test_IResourceURL_attributes_no_vroot(self):
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()
View
29 pyramid/tests/test_url.py
@@ -441,6 +441,31 @@ def test_route_url_with_anchor_app_url_elements_and_query(self):
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 @@ def test_current_route_url_with_request_query_duplicate_entries(self):
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 @@ def get_route(self, route_name):
class DummyRoute:
pregenerator = None
name = 'route'
+ remainder_name = None
def __init__(self, result='/1/2/3'):
self.result = result
View
16 pyramid/traversal.py
@@ -733,23 +733,33 @@ class ResourceURL(object):
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)
# 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
View
85 pyramid/url.py
@@ -192,6 +192,15 @@ def route_url(self, route_name, *elements, **kw):
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 @@ def route_url(self, route_name, *elements, **kw):
if route.pregenerator is not None:
elements, kw = route.pregenerator(self, elements, kw)
+ remainder_name = route.remainder_name
anchor = ''
qs = ''
app_url = None
@@ -248,6 +258,16 @@ def route_url(self, route_name, *elements, **kw):
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
if elements:
@@ -400,9 +420,48 @@ def resource_url(self, resource, *elements, **kw):
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 @@ def resource_url(self, resource, *elements, **kw):
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']
View
9 pyramid/urldispatch.py
@@ -33,6 +33,7 @@ def __init__(self, name, pattern, factory=None, predicates=(),
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 @@ def __call__(self, request):
# 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 @@ def generator(dict):
return result
return matcher, generator
+
+def get_remainder_name(pattern):
+ match = star_at_end.search(pattern)
+ if match:
+ return match.groups()[0]
+

0 comments on commit db0185f

Please sign in to comment.
Something went wrong with that request. Please try again.