summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt39
-rw-r--r--docs/narr/hybrid.rst102
-rw-r--r--pyramid/httpexceptions.py56
-rw-r--r--pyramid/interfaces.py15
-rw-r--r--pyramid/tests/pkgs/exceptionviewapp/__init__.py8
-rw-r--r--pyramid/tests/pkgs/exceptionviewapp/views.py7
-rw-r--r--pyramid/tests/test_httpexceptions.py6
-rw-r--r--pyramid/tests/test_integration.py4
-rw-r--r--pyramid/tests/test_traversal.py27
-rw-r--r--pyramid/tests/test_url.py152
-rw-r--r--pyramid/traversal.py16
-rw-r--r--pyramid/url.py87
-rw-r--r--pyramid/urldispatch.py2
13 files changed, 477 insertions, 44 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 93349abe6..b3c4e6a60 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -4,6 +4,19 @@ 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
+ 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,11 +164,17 @@ 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
---------
+- 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
@@ -232,6 +251,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)
================
@@ -901,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/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/httpexceptions.py b/pyramid/httpexceptions.py
index d8832570b..fff17b2df 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
@@ -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,23 +306,30 @@ class HTTPRedirection(WSGIHTTPException):
condition.
"""
-class HTTPOk(WSGIHTTPException):
+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):
+ """
+ subclass of :class:`~HTTPSuccessful`
+
+ Indicates that the request has succeeded.
+
+ code: 200, title: OK
+ """
+ code = 200
+ title = 'OK'
+
+class HTTPCreated(HTTPSuccessful):
"""
- subclass of :class:`~HTTPOk`
+ subclass of :class:`~HTTPSuccessful`
This indicates that request has been fulfilled and resulted in a new
resource being created.
@@ -333,9 +339,9 @@ class HTTPCreated(HTTPOk):
code = 201
title = 'Created'
-class HTTPAccepted(HTTPOk):
+class HTTPAccepted(HTTPSuccessful):
"""
- subclass of :class:`~HTTPOk`
+ subclass of :class:`~HTTPSuccessful`
This indicates that the request has been accepted for processing, but the
processing has not been completed.
@@ -346,9 +352,9 @@ class HTTPAccepted(HTTPOk):
title = 'Accepted'
explanation = 'The request is accepted for processing.'
-class HTTPNonAuthoritativeInformation(HTTPOk):
+class HTTPNonAuthoritativeInformation(HTTPSuccessful):
"""
- subclass of :class:`~HTTPOk`
+ subclass of :class:`~HTTPSuccessful`
This indicates that the returned metainformation in the entity-header is
not the definitive set as available from the origin server, but is
@@ -359,9 +365,9 @@ class HTTPNonAuthoritativeInformation(HTTPOk):
code = 203
title = 'Non-Authoritative Information'
-class HTTPNoContent(HTTPOk):
+class HTTPNoContent(HTTPSuccessful):
"""
- subclass of :class:`~HTTPOk`
+ subclass of :class:`~HTTPSuccessful`
This indicates that the server has fulfilled the request but does
not need to return an entity-body, and might want to return updated
@@ -373,9 +379,9 @@ class HTTPNoContent(HTTPOk):
title = 'No Content'
empty_body = True
-class HTTPResetContent(HTTPOk):
+class HTTPResetContent(HTTPSuccessful):
"""
- subclass of :class:`~HTTPOk`
+ subclass of :class:`~HTTPSuccessful`
This indicates that the server has fulfilled the request and
the user agent SHOULD reset the document view which caused the
@@ -387,9 +393,9 @@ class HTTPResetContent(HTTPOk):
title = 'Reset Content'
empty_body = True
-class HTTPPartialContent(HTTPOk):
+class HTTPPartialContent(HTTPSuccessful):
"""
- subclass of :class:`~HTTPOk`
+ subclass of :class:`~HTTPSuccessful`
This indicates that the server has fulfilled the partial GET
request for the resource.
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/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):
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