diff options
| author | Chris McDonough <chrism@plope.com> | 2013-08-30 00:01:53 -0400 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2013-08-30 00:01:53 -0400 |
| commit | 8c5ccaf633240b397a1a6d18c02d6c6fce26b8c9 (patch) | |
| tree | 7826c387f61d17513162b1e47ee1e19723a1cc6a | |
| parent | 330164c3190d92a3e1df89baafba12570d03bd32 (diff) | |
| parent | c29603ed0d8fd0b55789eb8f975c901961864d66 (diff) | |
| download | pyramid-8c5ccaf633240b397a1a6d18c02d6c6fce26b8c9.tar.gz pyramid-8c5ccaf633240b397a1a6d18c02d6c6fce26b8c9.tar.bz2 pyramid-8c5ccaf633240b397a1a6d18c02d6c6fce26b8c9.zip | |
Merge branch 'feature.hybridurlgen'
| -rw-r--r-- | CHANGES.txt | 19 | ||||
| -rw-r--r-- | docs/narr/hybrid.rst | 102 | ||||
| -rw-r--r-- | pyramid/interfaces.py | 15 | ||||
| -rw-r--r-- | pyramid/tests/test_request.py | 1 | ||||
| -rw-r--r-- | pyramid/tests/test_traversal.py | 27 | ||||
| -rw-r--r-- | pyramid/tests/test_url.py | 152 | ||||
| -rw-r--r-- | pyramid/traversal.py | 16 | ||||
| -rw-r--r-- | pyramid/url.py | 87 | ||||
| -rw-r--r-- | pyramid/urldispatch.py | 2 |
9 files changed, 405 insertions, 16 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 93349abe6..d3d3f64ce 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,12 @@ Next Release Features -------- +- 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,7 +157,8 @@ Features - 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 --------- @@ -232,6 +239,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) ================ diff --git a/docs/narr/hybrid.rst b/docs/narr/hybrid.rst index 1773a6b8c..a29ccb2ac 100644 --- a/docs/narr/hybrid.rst +++ b/docs/narr/hybrid.rst @@ -549,3 +549,105 @@ be invoked when the request URI is ``/abc/bazbuz``, assuming there is 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``. + diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 2a14df7c7..3f43494a8 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -692,6 +692,7 @@ class IRoute(Interface): 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 @@ class IRoutesMapper(Interface): 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. diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index 565c6377e..4f1c23e7b 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -594,6 +594,7 @@ class DummyRoutesMapper: class DummyRoute: pregenerator = None + remainder_name = None def __init__(self, result='/1/2/3'): self.result = result diff --git a/pyramid/tests/test_traversal.py b/pyramid/tests/test_traversal.py index ba0be7e06..ff5937811 100644 --- a/pyramid/tests/test_traversal.py +++ b/pyramid/tests/test_traversal.py @@ -1063,7 +1063,28 @@ class ResourceURLTests(unittest.TestCase): 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 @@ class ResourceURLTests(unittest.TestCase): 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() diff --git a/pyramid/tests/test_url.py b/pyramid/tests/test_url.py index 6f1ee3bf0..f6117777f 100644 --- a/pyramid/tests/test_url.py +++ b/pyramid/tests/test_url.py @@ -46,11 +46,12 @@ class TestURLMethodsMixin(unittest.TestCase): 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 @@ class TestURLMethodsMixin(unittest.TestCase): 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 @@ class TestURLMethodsMixin(unittest.TestCase): 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 diff --git a/pyramid/traversal.py b/pyramid/traversal.py index 469e77454..341ed2d75 100644 --- a/pyramid/traversal.py +++ b/pyramid/traversal.py @@ -733,11 +733,15 @@ 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) @@ -745,11 +749,17 @@ class ResourceURL(object): # 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 diff --git a/pyramid/url.py b/pyramid/url.py index 3d95d7cc9..fda2c72c7 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -401,10 +401,66 @@ class URLMethodsMixin(object): 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 @@ class URLMethodsMixin(object): 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'] diff --git a/pyramid/urldispatch.py b/pyramid/urldispatch.py index 8090f07f2..fe4d433c3 100644 --- a/pyramid/urldispatch.py +++ b/pyramid/urldispatch.py @@ -91,7 +91,7 @@ class RoutesMapper(object): # 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 |
