From db0185ff8516b852aad0a1bdb0cbcee63d28c4d2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 29 Aug 2013 18:07:04 -0400 Subject: first cut at hybrid url generation; still needs tests for resource_url logic --- CHANGES.txt | 10 +++++ pyramid/interfaces.py | 24 +++++++++++- pyramid/tests/test_request.py | 1 + pyramid/tests/test_traversal.py | 27 ++++++++++++- pyramid/tests/test_url.py | 29 +++++++++++++- pyramid/traversal.py | 16 ++++++-- pyramid/url.py | 85 ++++++++++++++++++++++++++++++++++++++++- pyramid/urldispatch.py | 9 ++++- 8 files changed, 190 insertions(+), 11 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 93349abe6..657c50009 100644 --- a/CHANGES.txt +++ b/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) ================ diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 2a14df7c7..b31edd0e3 100644 --- a/pyramid/interfaces.py +++ b/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 @@ 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 = 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..8a55ab328 100644 --- a/pyramid/tests/test_url.py +++ b/pyramid/tests/test_url.py @@ -441,6 +441,31 @@ class TestURLMethodsMixin(unittest.TestCase): 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 @@ 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 @@ -1113,6 +1139,7 @@ class DummyRoutesMapper: class DummyRoute: pregenerator = None name = 'route' + remainder_name = None def __init__(self, result='/1/2/3'): self.result = result 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..c2009e773 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -192,6 +192,15 @@ class URLMethodsMixin(object): 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 @@ class URLMethodsMixin(object): 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 @@ class URLMethodsMixin(object): 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 @@ class URLMethodsMixin(object): 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 @@ class URLMethodsMixin(object): 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'] diff --git a/pyramid/urldispatch.py b/pyramid/urldispatch.py index 8090f07f2..621b6d939 100644 --- a/pyramid/urldispatch.py +++ b/pyramid/urldispatch.py @@ -33,6 +33,7 @@ class Route(object): 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 @@ 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 @@ -233,3 +234,9 @@ def _compile_route(route): return result return matcher, generator + +def get_remainder_name(pattern): + match = star_at_end.search(pattern) + if match: + return match.groups()[0] + -- cgit v1.2.3 From 60aeef08eb740a99aae54cd7797a20cd328e6413 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 29 Aug 2013 21:25:34 -0400 Subject: resource_url coverage --- pyramid/tests/test_url.py | 135 ++++++++++++++++++++++++++++++++++++++++++++-- pyramid/url.py | 17 +++--- 2 files changed, 142 insertions(+), 10 deletions(-) diff --git a/pyramid/tests/test_url.py b/pyramid/tests/test_url.py index 8a55ab328..30dad86fd 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,134 @@ 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') + route.remainder_name = 'fred' + 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, {'fred': ('', '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') + route.remainder_name = 'fred' + 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, {'fred': ('', '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') + route.remainder_name = 'fred' + 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, {'fred': ('', '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') + route.remainder_name = 'fred' + 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, {'fred': ('', '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') + route.remainder_name = 'fred' + 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, + {'fred': ('', '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') + route.remainder_name = 'fred' + 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, {'fred': ('', 'a', 'b', 'c', '')}) def test_resource_path(self): request = self._makeOne() diff --git a/pyramid/url.py b/pyramid/url.py index c2009e773..d8fe997d9 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -453,8 +453,10 @@ class URLMethodsMixin(object): **kw, ) - If route_kw is passed, but route_name is not passed, a - :exc:`ValueError` will be raised. + If ``route_kw`` is passed, but ``route_name`` is not passed, + ``route_kw`` will be ignored. If ``route_name`` is passed, the + ``__resource_url__`` method of the resource passed is ignored + unconditionally. The ``route_name`` and ``route_kw`` arguments were added in Pyramid 1.5. @@ -511,7 +513,7 @@ 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 @@ -520,14 +522,16 @@ class URLMethodsMixin(object): if 'route_name' in kw: newkw = {} route_name = kw['route_name'] - remainder = getattr(resource_url, 'virtual_path_tuple', None) + 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(resource_url.virtual_path.split('/')) + remainder = tuple(url_adapter.virtual_path.split('/')) newkw['_remainder'] = remainder - for name in ('app_url', 'scheme', 'host', 'port'): + for name in ( + 'app_url', 'scheme', 'host', 'port', 'query', 'anchor' + ): val = kw.get(name, None) if val is not None: newkw['_' + name] = val @@ -536,7 +540,6 @@ class URLMethodsMixin(object): 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: -- cgit v1.2.3 From 0a4aed1c8e0a6da9219cccb6f55882d916f49916 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 29 Aug 2013 21:58:48 -0400 Subject: documentation for hybrid url generation --- CHANGES.txt | 6 ++++ docs/narr/hybrid.rst | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++ pyramid/url.py | 18 ++++++------ 3 files changed, 95 insertions(+), 9 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 657c50009..cfa77283f 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`` and ``route_kw`` 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 diff --git a/docs/narr/hybrid.rst b/docs/narr/hybrid.rst index 1773a6b8c..58d89fc98 100644 --- a/docs/narr/hybrid.rst +++ b/docs/narr/hybrid.rst @@ -549,3 +549,83 @@ 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`` and ``route_kw``. + +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. + +All other values that are normally passable to ``resource_path`` and +``resource_url`` (such as ``query``, ``anchor``, ``host``, ``port``, etc) work +as you might expect in this configuration too. + +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. + +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/url.py b/pyramid/url.py index d8fe997d9..90b8f667f 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -420,6 +420,14 @@ class URLMethodsMixin(object): are also passed, ``app_url`` will take precedence and the values 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) for the + base resource which is operated upon by this function. See also + :ref:`overriding_resource_url_generation`. + + .. versionadded:: 1.5 + ``route_name`` and ``route_kw`` + 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}, @@ -436,7 +444,7 @@ class URLMethodsMixin(object): 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. + value 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 @@ -458,14 +466,6 @@ class URLMethodsMixin(object): ``__resource_url__`` method of the resource passed is ignored unconditionally. - 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) for the - base resource which is operated upon by this function. See also - :ref:`overriding_resource_url_generation`. - .. note:: If the :term:`resource` used is the result of a :term:`traversal`, it -- cgit v1.2.3 From 67674f9609514dc80c15d617f71ef523a2377cfd Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 29 Aug 2013 22:23:49 -0400 Subject: point at docs section --- CHANGES.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index cfa77283f..70bc31840 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -157,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 --------- -- cgit v1.2.3 From e744e9c33b076c9e078e0b8b53c600b6db46c831 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 29 Aug 2013 22:31:57 -0400 Subject: fix name --- pyramid/interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index b31edd0e3..0f3f9fa1e 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -757,7 +757,7 @@ class IResourceURL(Interface): virtual_path_tuple = Attribute( 'The virtual url path of the resource as a tuple. (New in 1.5)' ) - physical_path = Attribute( + physical_path_tuple = Attribute( 'The physical url path of the resource as a tuple. (New in 1.5)' ) -- cgit v1.2.3 From 06f57f4616758f1f05cb0402fc970089d4483f28 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 29 Aug 2013 23:03:56 -0400 Subject: fix the regex used by get_remainder_name, add tests for get_remainder_name --- pyramid/tests/test_urldispatch.py | 11 +++++++++++ pyramid/urldispatch.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pyramid/tests/test_urldispatch.py b/pyramid/tests/test_urldispatch.py index 1755d9f47..ba5f2eeb5 100644 --- a/pyramid/tests/test_urldispatch.py +++ b/pyramid/tests/test_urldispatch.py @@ -522,6 +522,17 @@ class TestCompileRouteFunctional(unittest.TestCase): self.generates('/foo/:_abc', {'_abc':'20'}, '/foo/20') self.generates('/foo/:abc_def', {'abc_def':'20'}, '/foo/20') +class Test_get_remainder_name(unittest.TestCase): + def _callFUT(self, pattern): + from pyramid.urldispatch import get_remainder_name + return get_remainder_name(pattern) + + def test_it_nostararg(self): + self.assertEqual(self._callFUT('/bob'), None) + + def test_it_withstararg(self): + self.assertEqual(self._callFUT('/bob*dean'), 'dean') + class DummyContext(object): """ """ diff --git a/pyramid/urldispatch.py b/pyramid/urldispatch.py index 621b6d939..75bff904d 100644 --- a/pyramid/urldispatch.py +++ b/pyramid/urldispatch.py @@ -92,7 +92,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 -- cgit v1.2.3 From c29603ed0d8fd0b55789eb8f975c901961864d66 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 29 Aug 2013 23:55:36 -0400 Subject: get rid of remainder_name on route, and just default to passing traverse; add route_remainder_name argument to resource_url --- CHANGES.txt | 8 ++--- docs/narr/hybrid.rst | 30 ++++++++++++++++--- pyramid/interfaces.py | 9 ------ pyramid/tests/test_url.py | 62 ++++++++++++++++----------------------- pyramid/tests/test_urldispatch.py | 11 ------- pyramid/url.py | 55 ++++++++++++++++------------------ pyramid/urldispatch.py | 7 ----- 7 files changed, 81 insertions(+), 101 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 70bc31840..d3d3f64ce 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -5,10 +5,10 @@ Features -------- - You can now generate "hybrid" urldispatch/traversal URLs more easily - by using the new ``route_name`` and ``route_kw`` 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". + 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 diff --git a/docs/narr/hybrid.rst b/docs/narr/hybrid.rst index 58d89fc98..a29ccb2ac 100644 --- a/docs/narr/hybrid.rst +++ b/docs/narr/hybrid.rst @@ -563,7 +563,8 @@ Generating Hybrid URLs 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`` and ``route_kw``. +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``) @@ -615,15 +616,36 @@ You can pass ``route_kw`` in to fill in ``{id}`` above: If you pass ``route_kw`` but do not pass ``route_name``, ``route_kw`` will be ignored. -All other values that are normally passable to ``resource_path`` and -``resource_url`` (such as ``query``, ``anchor``, ``host``, ``port``, etc) work -as you might expect in this configuration too. +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 diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 0f3f9fa1e..3f43494a8 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -692,15 +692,6 @@ 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): """ diff --git a/pyramid/tests/test_url.py b/pyramid/tests/test_url.py index 30dad86fd..f6117777f 100644 --- a/pyramid/tests/test_url.py +++ b/pyramid/tests/test_url.py @@ -269,13 +269,12 @@ class TestURLMethodsMixin(unittest.TestCase): # no virtual_path_tuple on adapter adapter.virtual_path = '/a/b/c/' route = DummyRoute('/1/2/3') - route.remainder_name = 'fred' 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, {'fred': ('', 'a', 'b', 'c', '')}) + self.assertEqual(route.kw, {'traverse': ('', 'a', 'b', 'c', '')}) def test_resource_url_with_route_name_remainder_on_adapter(self): from pyramid.interfaces import IRoutesMapper @@ -289,13 +288,12 @@ class TestURLMethodsMixin(unittest.TestCase): # virtual_path_tuple on adapter adapter.virtual_path_tuple = ('', 'a', 'b', 'c', '') route = DummyRoute('/1/2/3') - route.remainder_name = 'fred' 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, {'fred': ('', 'a', 'b', 'c', '')}) + self.assertEqual(route.kw, {'traverse': ('', 'a', 'b', 'c', '')}) def test_resource_url_with_route_name_and_app_url(self): from pyramid.interfaces import IRoutesMapper @@ -309,13 +307,12 @@ class TestURLMethodsMixin(unittest.TestCase): # virtual_path_tuple on adapter adapter.virtual_path_tuple = ('', 'a', 'b', 'c', '') route = DummyRoute('/1/2/3') - route.remainder_name = 'fred' 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, {'fred': ('', 'a', 'b', 'c', '')}) + 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 @@ -329,7 +326,6 @@ class TestURLMethodsMixin(unittest.TestCase): # virtual_path_tuple on adapter adapter.virtual_path_tuple = ('', 'a', 'b', 'c', '') route = DummyRoute('/1/2/3') - route.remainder_name = 'fred' mapper = DummyRoutesMapper(route) request.registry.registerUtility(mapper, IRoutesMapper) root = DummyContext() @@ -337,7 +333,7 @@ class TestURLMethodsMixin(unittest.TestCase): 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, {'fred': ('', 'a', 'b', 'c', '')}) + self.assertEqual(route.kw, {'traverse': ('', 'a', 'b', 'c', '')}) def test_resource_url_with_route_name_and_route_kwargs(self): from pyramid.interfaces import IRoutesMapper @@ -351,7 +347,6 @@ class TestURLMethodsMixin(unittest.TestCase): # virtual_path_tuple on adapter adapter.virtual_path_tuple = ('', 'a', 'b', 'c', '') route = DummyRoute('/1/2/3') - route.remainder_name = 'fred' mapper = DummyRoutesMapper(route) request.registry.registerUtility(mapper, IRoutesMapper) root = DummyContext() @@ -360,7 +355,7 @@ class TestURLMethodsMixin(unittest.TestCase): self.assertEqual(result, 'http://example.com:5432/1/2/3') self.assertEqual( route.kw, - {'fred': ('', 'a', 'b', 'c', ''), + {'traverse': ('', 'a', 'b', 'c', ''), 'a':'1', 'b':'2'} ) @@ -377,12 +372,31 @@ class TestURLMethodsMixin(unittest.TestCase): # virtual_path_tuple on adapter adapter.virtual_path_tuple = ('', 'a', 'b', 'c', '') route = DummyRoute('/1/2/3') - route.remainder_name = 'fred' 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): @@ -570,31 +584,6 @@ class TestURLMethodsMixin(unittest.TestCase): 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 @@ -1268,7 +1257,6 @@ class DummyRoutesMapper: class DummyRoute: pregenerator = None name = 'route' - remainder_name = None def __init__(self, result='/1/2/3'): self.result = result diff --git a/pyramid/tests/test_urldispatch.py b/pyramid/tests/test_urldispatch.py index ba5f2eeb5..1755d9f47 100644 --- a/pyramid/tests/test_urldispatch.py +++ b/pyramid/tests/test_urldispatch.py @@ -522,17 +522,6 @@ class TestCompileRouteFunctional(unittest.TestCase): self.generates('/foo/:_abc', {'_abc':'20'}, '/foo/20') self.generates('/foo/:abc_def', {'abc_def':'20'}, '/foo/20') -class Test_get_remainder_name(unittest.TestCase): - def _callFUT(self, pattern): - from pyramid.urldispatch import get_remainder_name - return get_remainder_name(pattern) - - def test_it_nostararg(self): - self.assertEqual(self._callFUT('/bob'), None) - - def test_it_withstararg(self): - self.assertEqual(self._callFUT('/bob*dean'), 'dean') - class DummyContext(object): """ """ diff --git a/pyramid/url.py b/pyramid/url.py index 90b8f667f..fda2c72c7 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -192,15 +192,6 @@ class URLMethodsMixin(object): 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. @@ -222,7 +213,6 @@ class URLMethodsMixin(object): if route.pregenerator is not None: elements, kw = route.pregenerator(self, elements, kw) - remainder_name = route.remainder_name anchor = '' qs = '' app_url = None @@ -258,16 +248,6 @@ class URLMethodsMixin(object): 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: @@ -426,7 +406,7 @@ class URLMethodsMixin(object): :ref:`overriding_resource_url_generation`. .. versionadded:: 1.5 - ``route_name`` and ``route_kw`` + ``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 @@ -439,21 +419,34 @@ class URLMethodsMixin(object): 'element1', 'element2', _query={'a':'1'}, - _remainder=remainder_path, + 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 equivalent to:: + _query={'a':'1'})`` is roughly equivalent to:: remainder_path = request.resource_path_tuple(someobject) - kw = {'id':'4', '_query':{'a':'1'}, '_remainder':remainder_path} + kw = {'id':'4', '_query':{'a':'1'}, 'traverse':traversal_path} url = request.route_url( 'blogentry', 'element1', @@ -461,10 +454,12 @@ class URLMethodsMixin(object): **kw, ) - If ``route_kw`` is passed, but ``route_name`` is not passed, - ``route_kw`` will be ignored. If ``route_name`` is passed, the - ``__resource_url__`` method of the resource passed is ignored - unconditionally. + 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:: @@ -527,7 +522,8 @@ class URLMethodsMixin(object): # older user-supplied IResourceURL adapter without 1.5 # virtual_path_tuple remainder = tuple(url_adapter.virtual_path.split('/')) - newkw['_remainder'] = remainder + remainder_name = kw.get('route_remainder_name', 'traverse') + newkw[remainder_name] = remainder for name in ( 'app_url', 'scheme', 'host', 'port', 'query', 'anchor' @@ -540,6 +536,7 @@ class URLMethodsMixin(object): 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: diff --git a/pyramid/urldispatch.py b/pyramid/urldispatch.py index 75bff904d..fe4d433c3 100644 --- a/pyramid/urldispatch.py +++ b/pyramid/urldispatch.py @@ -33,7 +33,6 @@ class Route(object): 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 @@ -234,9 +233,3 @@ def _compile_route(route): return result return matcher, generator - -def get_remainder_name(pattern): - match = star_at_end.search(pattern) - if match: - return match.groups()[0] - -- cgit v1.2.3 From 097b4915f490bad988333a69377944e0b64a65f5 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 30 Aug 2013 00:26:32 -0400 Subject: unused code --- pyramid/tests/test_request.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index 4f1c23e7b..565c6377e 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -594,7 +594,6 @@ class DummyRoutesMapper: class DummyRoute: pregenerator = None - remainder_name = None def __init__(self, result='/1/2/3'): self.result = result -- cgit v1.2.3 From 97ed56d766298ee042305ff8712df5f1fc3fbe3a Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 30 Aug 2013 11:48:56 -0400 Subject: allow exception view registrations for HTTPException to override default exception view; closes #985 --- CHANGES.txt | 5 +++++ pyramid/httpexceptions.py | 15 +++++++-------- pyramid/tests/pkgs/exceptionviewapp/__init__.py | 8 ++++++++ pyramid/tests/pkgs/exceptionviewapp/views.py | 7 +++++++ pyramid/tests/test_httpexceptions.py | 6 +++--- pyramid/tests/test_integration.py | 4 ++++ 6 files changed, 34 insertions(+), 11 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index d3d3f64ce..334785424 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -163,6 +163,11 @@ Features 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 (e.g. ``foo#macroname.pt``), renderings of the template after the template diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index d8832570b..10568b26e 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -149,11 +149,8 @@ def _no_escape(value): 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 @@ ${body}''') '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 @@ ${body}''') 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 @@ class HTTPError(WSGIHTTPException): 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,7 +306,7 @@ class HTTPRedirection(WSGIHTTPException): condition. """ -class HTTPOk(WSGIHTTPException): +class HTTPOk(HTTPException): """ Base class for exceptions with status codes in the 200s (successful responses) diff --git a/pyramid/tests/pkgs/exceptionviewapp/__init__.py b/pyramid/tests/pkgs/exceptionviewapp/__init__.py index f169e0cd5..ffc1b47c6 100644 --- a/pyramid/tests/pkgs/exceptionviewapp/__init__.py +++ b/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 @@ def includeme(config): 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) + + diff --git a/pyramid/tests/pkgs/exceptionviewapp/views.py b/pyramid/tests/pkgs/exceptionviewapp/views.py index 33b97671e..4953056bc 100644 --- a/pyramid/tests/pkgs/exceptionviewapp/views.py +++ b/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 whoa(request): def raise_exception(request): raise AnException() + +def raise_httpexception(request): + raise HTTPBadRequest + +def catch_httpexception(request): + return Response('caught') diff --git a/pyramid/tests/test_httpexceptions.py b/pyramid/tests/test_httpexceptions.py index 0061907ba..d0779e080 100644 --- a/pyramid/tests/test_httpexceptions.py +++ b/pyramid/tests/test_httpexceptions.py @@ -57,10 +57,10 @@ class Test__no_escape(unittest.TestCase): 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): diff --git a/pyramid/tests/test_integration.py b/pyramid/tests/test_integration.py index eda4ae9f3..391310432 100644 --- a/pyramid/tests/test_integration.py +++ b/pyramid/tests/test_integration.py @@ -465,6 +465,10 @@ class TestExceptionViewsApp(IntegrationBase, unittest.TestCase): 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): -- cgit v1.2.3 From c5ed5491e8a6d736bedba45b2cb944799f36a5e4 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 30 Aug 2013 12:18:57 -0400 Subject: add HTTPSuccessful base class, allowing HTTPOk to be caught independently; closes #986 --- CHANGES.txt | 15 +++++++++++++++ pyramid/httpexceptions.py | 30 ++++++++++++++++++------------ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 334785424..b3c4e6a60 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,13 @@ Next Release 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 @@ -923,6 +930,14 @@ Backwards Incompatibilities 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 ------------- diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index 10568b26e..fca4157b5 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -13,7 +13,7 @@ and 500s are server errors. Exception HTTPException - HTTPOk + HTTPSuccessful * 200 - HTTPOk * 201 - HTTPCreated * 202 - HTTPAccepted @@ -306,21 +306,27 @@ class HTTPRedirection(HTTPException): condition. """ -class HTTPOk(HTTPException): +class HTTPSuccessful(HTTPException): """ Base class for exceptions with status codes in the 200s (successful responses) - - code: 200, title: OK """ - code = 200 - title = 'OK' ############################################################ ## 2xx success ############################################################ -class HTTPCreated(HTTPOk): +class HTTPOk(HTTPSuccessful): + """ + Base class for exceptions with status codes in the 200s (successful + responses) + + code: 200, title: OK + """ + code = 200 + title = 'OK' + +class HTTPCreated(HTTPSuccessful): """ subclass of :class:`~HTTPOk` @@ -332,7 +338,7 @@ class HTTPCreated(HTTPOk): code = 201 title = 'Created' -class HTTPAccepted(HTTPOk): +class HTTPAccepted(HTTPSuccessful): """ subclass of :class:`~HTTPOk` @@ -345,7 +351,7 @@ class HTTPAccepted(HTTPOk): title = 'Accepted' explanation = 'The request is accepted for processing.' -class HTTPNonAuthoritativeInformation(HTTPOk): +class HTTPNonAuthoritativeInformation(HTTPSuccessful): """ subclass of :class:`~HTTPOk` @@ -358,7 +364,7 @@ class HTTPNonAuthoritativeInformation(HTTPOk): code = 203 title = 'Non-Authoritative Information' -class HTTPNoContent(HTTPOk): +class HTTPNoContent(HTTPSuccessful): """ subclass of :class:`~HTTPOk` @@ -372,7 +378,7 @@ class HTTPNoContent(HTTPOk): title = 'No Content' empty_body = True -class HTTPResetContent(HTTPOk): +class HTTPResetContent(HTTPSuccessful): """ subclass of :class:`~HTTPOk` @@ -386,7 +392,7 @@ class HTTPResetContent(HTTPOk): title = 'Reset Content' empty_body = True -class HTTPPartialContent(HTTPOk): +class HTTPPartialContent(HTTPSuccessful): """ subclass of :class:`~HTTPOk` -- cgit v1.2.3 From 6926fac06b60af2cfc43d768c17df8a2cd1e1b2e Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 30 Aug 2013 12:21:21 -0400 Subject: fix docstrings --- pyramid/httpexceptions.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index fca4157b5..d05e419a0 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -328,7 +328,7 @@ class HTTPOk(HTTPSuccessful): 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. @@ -340,7 +340,7 @@ class HTTPCreated(HTTPSuccessful): 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. @@ -353,7 +353,7 @@ class HTTPAccepted(HTTPSuccessful): 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 @@ -366,7 +366,7 @@ class HTTPNonAuthoritativeInformation(HTTPSuccessful): 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 @@ -380,7 +380,7 @@ class HTTPNoContent(HTTPSuccessful): 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 @@ -394,7 +394,7 @@ class HTTPResetContent(HTTPSuccessful): 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. -- cgit v1.2.3 From 0c93e5f878f3c955e2f146807b6b80c29b1d41d5 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 30 Aug 2013 12:25:25 -0400 Subject: fix httpok docstring --- pyramid/httpexceptions.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index d05e419a0..e6ae6074d 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -318,8 +318,9 @@ class HTTPSuccessful(HTTPException): class HTTPOk(HTTPSuccessful): """ - Base class for exceptions with status codes in the 200s (successful - responses) + subclass of :class:`~HTTPSuccessful` + + Indicates that the request has suceeded. code: 200, title: OK """ -- cgit v1.2.3 From 23f1ff130c55b3c33ededb85480fc57b633d739d Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 30 Aug 2013 12:26:12 -0400 Subject: dammit, typo --- pyramid/httpexceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index e6ae6074d..fff17b2df 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -320,7 +320,7 @@ class HTTPOk(HTTPSuccessful): """ subclass of :class:`~HTTPSuccessful` - Indicates that the request has suceeded. + Indicates that the request has succeeded. code: 200, title: OK """ -- cgit v1.2.3