summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt54
-rw-r--r--docs/api/config.rst2
-rw-r--r--docs/api/interfaces.rst4
-rw-r--r--docs/api/request.rst2
-rw-r--r--docs/narr/hooks.rst59
-rw-r--r--docs/whatsnew-1.3.rst56
-rw-r--r--pyramid/config/factories.py77
-rw-r--r--pyramid/interfaces.py43
-rw-r--r--pyramid/tests/test_config/test_factories.py86
-rw-r--r--pyramid/tests/test_request.py25
-rw-r--r--pyramid/tests/test_url.py363
-rw-r--r--pyramid/traversal.py66
-rw-r--r--pyramid/url.py226
13 files changed, 931 insertions, 132 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 26d547ae6..22f8320f9 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -23,11 +23,17 @@ Features
something like "AttributeError: 'NoneType' object has no attribute
'rfind'".
-- Add ``pyramid.config.Configurator.set_traverser`` API method. See the
+- Add ``pyramid.config.Configurator.add_traverser`` API method. See the
Hooks narrative documentation section entitled "Changing the Traverser" for
more information. This is not a new feature, it just provides an API for
adding a traverser without needing to use the ZCA API.
+- Add ``pyramid.config.Configurator.add_resource_url_adapter`` API method.
+ See the Hooks narrative documentation section entitled "Changing How
+ pyramid.request.Request.resource_url Generates a URL" for more information.
+ This is not a new feature, it just provides an API for adding a resource
+ url adapter without needing to use the ZCA API.
+
- The system value ``req`` is now supplied to renderers as an alias for
``request``. This means that you can now, for example, in a template, do
``req.route_url(...)`` instead of ``request.route_url(...)``. This is
@@ -35,6 +41,52 @@ Features
methods and attributes from within templates. The value ``request`` is
still available too, this is just an alternative.
+- A new interface was added: ``pyramid.interfaces.IResourceURL``. An adapter
+ implementing its interface can be used to override resource URL generation
+ when ``request.resource_url`` is called. This interface replaces the
+ now-deprecated ``pyramid.interfaces.IContextURL`` interface.
+
+- The dictionary passed to a resource's ``__resource_url__`` method (see
+ "Overriding Resource URL Generation" in the "Resources" chapter) now
+ contains an ``app_url`` key, representing the application URL generated
+ during ``request.resource_url``. It represents a potentially customized
+ URL prefix, containing potentially custom scheme, host and port information
+ passed by the user to ``request.resource_url``. It should be used instead
+ of ``request.application_url`` where necessary.
+
+- The ``request.resource_url`` API now accepts these arguments: ``app_url``,
+ ``scheme``, ``host``, and ``port``. The app_url argument can be used to
+ replace the URL prefix wholesale during url generation. The ``scheme``,
+ ``host``, and ``port`` arguments can be used to replace the respective
+ default values of ``request.application_url`` partially.
+
+- A new API named ``request.resource_path`` now exists. It works like
+ ``request.resource_url`` but produces a relative URL rather than an
+ absolute one.
+
+- The ``request.route_url`` API now accepts these arguments: ``_app_url``,
+ ``_scheme``, ``_host``, and ``_port``. The ``_app_url`` argument can be
+ used to replace the URL prefix wholesale during url generation. The
+ ``_scheme``, ``_host``, and ``_port`` arguments can be used to replace the
+ respective default values of ``request.application_url`` partially.
+
+Backwards Incompatibilities
+---------------------------
+
+- The ``pyramid.interfaces.IContextURL`` interface has been deprecated.
+ People have been instructed to use this to register a resource url adapter
+ in the "Hooks" chapter to use to influence ``request.resource_url`` URL
+ generation for resources found via custom traversers since Pyramid 1.0.
+
+ The interface still exists and registering such an adapter still works, but
+ this interface will be removed from the software after a few major Pyramid
+ releases. You should replace it with an equivalent
+ ``pyramid.interfaces.IResourceURL`` adapter, registered using the new
+ ``pyramid.config.Configurator.add_resource_url_adapter`` API. A
+ deprecation warning is now emitted when a
+ ``pyramid.interfaces.IContextURL`` adapter is found when
+ ``request.resource_url`` is called.
+
Documentation
-------------
diff --git a/docs/api/config.rst b/docs/api/config.rst
index 3c5ee563a..3fc2cfc44 100644
--- a/docs/api/config.rst
+++ b/docs/api/config.rst
@@ -94,7 +94,7 @@
.. automethod:: set_notfound_view
- .. automethod:: set_traverser
+ .. automethod:: add_traverser
.. automethod:: set_renderer_globals_factory(factory)
diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst
index 11cd8cf7e..1dea5fab0 100644
--- a/docs/api/interfaces.rst
+++ b/docs/api/interfaces.rst
@@ -79,3 +79,7 @@ Other Interfaces
.. autointerface:: IAssetDescriptor
:members:
+
+ .. autointerface:: IResourceURL
+ :members:
+
diff --git a/docs/api/request.rst b/docs/api/request.rst
index 1ab84e230..e1b233fbc 100644
--- a/docs/api/request.rst
+++ b/docs/api/request.rst
@@ -183,6 +183,8 @@
.. automethod:: resource_url
+ .. automethod:: resource_path
+
.. attribute:: response_*
In Pyramid 1.0, you could set attributes on a
diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst
index 076f9fa5c..2c4310080 100644
--- a/docs/narr/hooks.rst
+++ b/docs/narr/hooks.rst
@@ -479,58 +479,55 @@ When you add a traverser as described in :ref:`changing_the_traverser`, it's
often convenient to continue to use the
:meth:`pyramid.request.Request.resource_url` API. However, since the way
traversal is done will have been modified, the URLs it generates by default
-may be incorrect.
+may be incorrect when used against resources derived from your custom
+traverser.
If you've added a traverser, you can change how
:meth:`~pyramid.request.Request.resource_url` generates a URL for a specific
-type of resource by adding a registerAdapter call for
-:class:`pyramid.interfaces.IContextURL` to your application:
+type of resource by adding a call to
+:meth:`pyramid.config.add_resource_url_adapter`.
+
+For example:
.. code-block:: python
:linenos:
- from pyramid.interfaces import ITraverser
- from zope.interface import Interface
- from myapp.traversal import URLGenerator
+ from myapp.traversal import ResourceURLAdapter
from myapp.resources import MyRoot
- config.registry.registerAdapter(URLGenerator, (MyRoot, Interface),
- IContextURL)
+ config.add_resource_url_adapter(ResourceURLAdapter, resource_iface=MyRoot)
-In the above example, the ``myapp.traversal.URLGenerator`` class will be used
-to provide services to :meth:`~pyramid.request.Request.resource_url` any time
-the :term:`context` passed to ``resource_url`` is of class
-``myapp.resources.MyRoot``. The second argument in the ``(MyRoot,
-Interface)`` tuple represents the type of interface that must be possessed by
-the :term:`request` (in this case, any interface, represented by
-``zope.interface.Interface``).
+In the above example, the ``myapp.traversal.ResourceURLAdapter`` class will
+be used to provide services to :meth:`~pyramid.request.Request.resource_url`
+any time the :term:`resource` passed to ``resource_url`` is of the class
+``myapp.resources.MyRoot``. The ``resource_iface`` argument ``MyRoot``
+represents the type of interface that must be possessed by the resource for
+this resource url factory to be found. If the ``resource_iface`` argument is
+omitted, this resource url adapter will be used for *all* resources.
-The API that must be implemented by a class that provides
-:class:`~pyramid.interfaces.IContextURL` is as follows:
+The API that must be implemented by your a class that provides
+:class:`~pyramid.interfaces.IResourceURL` is as follows:
.. code-block:: python
:linenos:
- from zope.interface import Interface
-
- class IContextURL(Interface):
- """ An adapter which deals with URLs related to a context.
+ class MyResourceURL(object):
+ """ An adapter which provides the virtual and physical paths of a
+ resource
"""
- def __init__(self, context, request):
- """ Accept the context and request """
-
- def virtual_root(self):
- """ Return the virtual root object related to a request and the
- current context"""
-
- def __call__(self):
- """ Return a URL that points to the context """
+ def __init__(self, resource, request):
+ """ Accept the resource and request and set self.physical_path and
+ self.virtual_path"""
+ self.virtual_path = some_function_of(resource, request)
+ self.physical_path = some_other_function_of(resource, request)
The default context URL generator is available for perusal as the class
-:class:`pyramid.traversal.TraversalContextURL` in the `traversal module
+:class:`pyramid.traversal.ResourceURL` in the `traversal module
<http://github.com/Pylons/pyramid/blob/master/pyramid/traversal.py>`_ of the
:term:`Pylons` GitHub Pyramid repository.
+See :meth:`pyramid.config.add_resource_url_adapter` for more information.
+
.. index::
single: IResponse
single: special view responses
diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst
index a27ef6af9..7d1c9217d 100644
--- a/docs/whatsnew-1.3.rst
+++ b/docs/whatsnew-1.3.rst
@@ -260,16 +260,21 @@ Minor Feature Additions
http://readthedocs.org/docs/venusian/en/latest/#ignore-scan-argument for
more information about how to use the ``ignore`` argument to ``scan``.
-- Add :meth:`pyramid.config.Configurator.set_traverser` API method. See
+- Add :meth:`pyramid.config.Configurator.add_traverser` API method. See
:ref:`changing_the_traverser` for more information. This is not a new
feature, it just provides an API for adding a traverser without needing to
use the ZCA API.
+- Add :meth:`pyramid.config.Configurator.add_resource_url_adapter` API
+ method. See :ref:`changing_resource_url` for more information. This is
+ not a new feature, it just provides an API for adding a resource url
+ adapter without needing to use the ZCA API.
+
- The :meth:`pyramid.config.Configurator.scan` method can now be passed an
``ignore`` argument, which can be a string, a callable, or a list
consisting of strings and/or callables. This feature allows submodules,
subpackages, and global objects from being scanned. See
- http://readthedocs.org/docs/venusian/en/latest/#ignore-scan-argument for
+ http://readthedocs.org/docs/venusian/en/latest/#ignore-scan-argument for
more information about how to use the ``ignore`` argument to ``scan``.
- Better error messages when a view callable returns a value that cannot be
@@ -291,6 +296,38 @@ Minor Feature Additions
methods and attributes from within templates. The value ``request`` is
still available too, this is just an alternative.
+- A new interface was added: :class:`pyramid.interfaces.IResourceURL`. An
+ adapter implementing its interface can be used to override resource URL
+ generation when :meth:`pyramid.request.Request.resource_url` is called.
+ This interface replaces the now-deprecated
+ ``pyramid.interfaces.IContextURL`` interface.
+
+- The dictionary passed to a resource's ``__resource_url__`` method (see
+ :ref:`overriding_resource_url_generation`) now contains an ``app_url`` key,
+ representing the application URL generated during
+ :meth:`pyramid.request.Request.resource_url`. It represents a potentially
+ customized URL prefix, containing potentially custom scheme, host and port
+ information passed by the user to ``request.resource_url``. It should be
+ used instead of ``request.application_url`` where necessary.
+
+- The :meth:`pyramid.request.Request.resource_url` API now accepts these
+ arguments: ``app_url``, ``scheme``, ``host``, and ``port``. The app_url
+ argument can be used to replace the URL prefix wholesale during url
+ generation. The ``scheme``, ``host``, and ``port`` arguments can be used
+ to replace the respective default values of ``request.application_url``
+ partially.
+
+- A new API named :meth:`pyramid.request.Request.resource_path` now exists.
+ It works like :meth:`pyramid.request.Request.resource_url`` but produces a
+ relative URL rather than an absolute one.
+
+- The :meth:`pyramid.request.Request.route_url` API now accepts these
+ arguments: ``_app_url``, ``_scheme``, ``_host``, and ``_port``. The
+ ``_app_url`` argument can be used to replace the URL prefix wholesale
+ during url generation. The ``_scheme``, ``_host``, and ``_port`` arguments
+ can be used to replace the respective default values of
+ ``request.application_url`` partially.
+
Backwards Incompatibilities
---------------------------
@@ -360,6 +397,21 @@ Backwards Incompatibilities
no negative affect because the implementation was broken for dict-based
arguments.
+- The ``pyramid.interfaces.IContextURL`` interface has been deprecated.
+ People have been instructed to use this to register a resource url adapter
+ in the "Hooks" chapter to use to influence
+ :meth:`pyramid.request.Request.resource_url` URL generation for resources
+ found via custom traversers since Pyramid 1.0.
+
+ The interface still exists and registering such an adapter still works, but
+ this interface will be removed from the software after a few major Pyramid
+ releases. You should replace it with an equivalent
+ :class:`pyramid.interfaces.IResourceURL` adapter, registered using the new
+ :meth:`pyramid.config.Configurator.add_resource_url_adapter` API. A
+ deprecation warning is now emitted when a
+ ``pyramid.interfaces.IContextURL`` adapter is found when
+ :meth:`pyramid.request.Request.resource_url` is called.
+
Documentation Enhancements
--------------------------
diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py
index 7c0ea054d..76f8d86ed 100644
--- a/pyramid/config/factories.py
+++ b/pyramid/config/factories.py
@@ -10,6 +10,7 @@ from pyramid.interfaces import (
IRootFactory,
ISessionFactory,
ITraverser,
+ IResourceURL,
)
from pyramid.traversal import DefaultRootFactory
@@ -143,7 +144,8 @@ class FactoriesConfiguratorMixin(object):
self.action(('request properties', name), register,
introspectables=(intr,))
- def set_traverser(self, factory, iface=None):
+ @action_method
+ def add_traverser(self, factory, iface=None):
"""
The superdefault :term:`traversal` algorithm that :app:`Pyramid` uses
is explained in :ref:`traversal_algorithm`. Though it is rarely
@@ -158,7 +160,7 @@ class FactoriesConfiguratorMixin(object):
.. code-block:: python
from myapp.traversal import MyCustomTraverser
- config.set_traverser(MyCustomTraverser)
+ config.add_traverser(MyCustomTraverser)
This would cause the Pyramid superdefault traverser to never be used;
intead all traversal would be done using your ``MyCustomTraverser``
@@ -186,7 +188,7 @@ class FactoriesConfiguratorMixin(object):
.. code-block:: python
- config.set_traverser(MyCustomTraverser, MyRootClass)
+ config.add_traverser(MyCustomTraverser, MyRootClass)
When more than one traverser is active, the "most specific" traverser
will be used (the one that matches the class or interface of the
@@ -212,7 +214,74 @@ class FactoriesConfiguratorMixin(object):
)
intr['factory'] = factory
intr['iface'] = iface
- self.action(('traverser', iface), register, introspectables=(intr,))
+ self.action(discriminator, register, introspectables=(intr,))
+
+ @action_method
+ def add_resource_url_adapter(self, factory, resource_iface=None,
+ request_iface=None):
+ """
+ When you add a traverser as described in
+ :ref:`changing_the_traverser`, it's convenient to continue to use the
+ :meth:`pyramid.request.Request.resource_url` API. However, since the
+ way traversal is done may have been modified, the URLs that
+ ``resource_url`` generates by default may be incorrect when resources
+ are returned by a custom traverser.
+
+ If you've added a traverser, you can change how
+ :meth:`~pyramid.request.Request.resource_url` generates a URL for a
+ specific type of resource by calling this method.
+
+ The ``factory`` argument represents a class that implements the
+ :class:`~pyramid.interfaces.IResourceURL` interface. The class
+ constructor should accept two arguments in its constructor (the
+ resource and the request) and the resulting instance should provide
+ the attributes detailed in that interface (``virtual_path`` and
+ ``physical_path``, in particular).
+
+ The ``resource_iface`` argument represents a class or interface that
+ the resource should possess for this url adapter to be used when
+ :meth:`pyramid.request.Request.resource_url` looks up a resource url
+ adapter. If ``resource_iface`` is not passed, or it is passed as
+ ``None``, the adapter will be used for every type of resource.
+
+ The ``request_iface`` argument represents a class or interface that
+ the request should possess for this url adapter to be used when
+ :meth:`pyramid.request.Request.resource_url` looks up a resource url
+ adapter. If ``request_iface`` is not epassed, or it is passed as
+ ``None``, the adapter will be used for every type of request.
+
+ See :ref:`changing_resource_url` for more information.
+
+ .. note::
+
+ This API is new in Pyramid 1.3.
+ """
+ factory = self.maybe_dotted(factory)
+ resource_iface = self.maybe_dotted(resource_iface)
+ request_iface = self.maybe_dotted(request_iface)
+ def register(resource_iface=resource_iface,
+ request_iface=request_iface):
+ if resource_iface is None:
+ resource_iface = Interface
+ if request_iface is None:
+ request_iface = Interface
+ self.registry.registerAdapter(
+ factory,
+ (resource_iface, request_iface),
+ IResourceURL,
+ )
+ discriminator = ('resource url adapter', resource_iface, request_iface)
+ intr = self.introspectable(
+ 'resource url adapters',
+ discriminator,
+ 'resource url adapter for resource iface %r, request_iface %r' % (
+ resource_iface, request_iface),
+ 'resource url adapter',
+ )
+ intr['factory'] = factory
+ intr['resource_iface'] = resource_iface
+ intr['request_iface'] = request_iface
+ self.action(discriminator, register, introspectables=(intr,))
def _set_request_properties(event):
request = event.request
diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py
index 8de5331b9..5b9edf31a 100644
--- a/pyramid/interfaces.py
+++ b/pyramid/interfaces.py
@@ -731,15 +731,54 @@ class IRoutesMapper(Interface):
``match`` key will be the matchdict or ``None`` if no route
matched. Static routes will not be considered for matching. """
-class IContextURL(Interface):
+class IResourceURL(Interface):
+ virtual_path = Attribute('The virtual url path of the resource.')
+ physical_path = Attribute('The physical url path of the resource.')
+
+class IContextURL(IResourceURL):
""" An adapter which deals with URLs related to a context.
+
+ ..warning::
+
+ This interface is deprecated as of Pyramid 1.3 with the introduction of
+ IResourceURL.
"""
+ # this class subclasses IResourceURL because request.resource_url looks
+ # for IResourceURL via queryAdapter. queryAdapter will find a deprecated
+ # IContextURL registration if no registration for IResourceURL exists.
+ # In reality, however, IContextURL objects were never required to have
+ # the virtual_path or physical_path attributes spelled in IResourceURL.
+ # The inheritance relationship is purely to benefit adapter lookup,
+ # not to imply an inheritance relationship of interface attributes
+ # and methods.
+ #
+ # Mechanics:
+ #
+ # class Fudge(object):
+ # def __init__(self, one, two):
+ # print one, two
+ # class Another(object):
+ # def __init__(self, one, two):
+ # print one, two
+ # ob = object()
+ # r.registerAdapter(Fudge, (Interface, Interface), IContextURL)
+ # print r.queryMultiAdapter((ob, ob), IResourceURL)
+ # r.registerAdapter(Another, (Interface, Interface), IResourceURL)
+ # print r.queryMultiAdapter((ob, ob), IResourceURL)
+ #
+ # prints
+ #
+ # <object object at 0x7fa678f3e2a0> <object object at 0x7fa678f3e2a0>
+ # <__main__.Fudge object at 0x1cda890>
+ # <object object at 0x7fa678f3e2a0> <object object at 0x7fa678f3e2a0>
+ # <__main__.Another object at 0x1cda850>
+
def virtual_root():
""" Return the virtual root related to a request and the
current context"""
def __call__():
- """ Return a URL that points to the context """
+ """ Return a URL that points to the context. """
class IPackageOverrides(Interface):
""" Utility for pkg_resources overrides """
diff --git a/pyramid/tests/test_config/test_factories.py b/pyramid/tests/test_config/test_factories.py
index 51c60896e..5f300a73e 100644
--- a/pyramid/tests/test_config/test_factories.py
+++ b/pyramid/tests/test_config/test_factories.py
@@ -129,10 +129,10 @@ class TestFactoriesMixin(unittest.TestCase):
self.assertEqual(callables, [('foo', foo, False),
('bar', foo, True)])
- def test_set_traverser_dotted_names(self):
+ def test_add_traverser_dotted_names(self):
from pyramid.interfaces import ITraverser
config = self._makeOne(autocommit=True)
- config.set_traverser(
+ config.add_traverser(
'pyramid.tests.test_config.test_factories.DummyTraverser',
'pyramid.tests.test_config.test_factories.DummyIface')
iface = DummyIface()
@@ -140,25 +140,25 @@ class TestFactoriesMixin(unittest.TestCase):
self.assertEqual(traverser.__class__, DummyTraverser)
self.assertEqual(traverser.root, iface)
- def test_set_traverser_default_iface_means_Interface(self):
+ def test_add_traverser_default_iface_means_Interface(self):
from pyramid.interfaces import ITraverser
config = self._makeOne(autocommit=True)
- config.set_traverser(DummyTraverser)
+ config.add_traverser(DummyTraverser)
traverser = config.registry.getAdapter(None, ITraverser)
self.assertEqual(traverser.__class__, DummyTraverser)
- def test_set_traverser_nondefault_iface(self):
+ def test_add_traverser_nondefault_iface(self):
from pyramid.interfaces import ITraverser
config = self._makeOne(autocommit=True)
- config.set_traverser(DummyTraverser, DummyIface)
+ config.add_traverser(DummyTraverser, DummyIface)
iface = DummyIface()
traverser = config.registry.getAdapter(iface, ITraverser)
self.assertEqual(traverser.__class__, DummyTraverser)
self.assertEqual(traverser.root, iface)
- def test_set_traverser_introspectables(self):
+ def test_add_traverser_introspectables(self):
config = self._makeOne()
- config.set_traverser(DummyTraverser, DummyIface)
+ config.add_traverser(DummyTraverser, DummyIface)
actions = config.action_state.actions
self.assertEqual(len(actions), 1)
intrs = actions[0]['introspectables']
@@ -169,6 +169,70 @@ class TestFactoriesMixin(unittest.TestCase):
self.assertEqual(intr.category_name, 'traversers')
self.assertEqual(intr.title, 'traverser for %r' % DummyIface)
+ def test_add_resource_url_adapter_dotted_names(self):
+ from pyramid.interfaces import IResourceURL
+ config = self._makeOne(autocommit=True)
+ config.add_resource_url_adapter(
+ 'pyramid.tests.test_config.test_factories.DummyResourceURL',
+ 'pyramid.tests.test_config.test_factories.DummyIface',
+ 'pyramid.tests.test_config.test_factories.DummyIface',
+ )
+ iface = DummyIface()
+ adapter = config.registry.getMultiAdapter((iface, iface),
+ IResourceURL)
+ self.assertEqual(adapter.__class__, DummyResourceURL)
+ self.assertEqual(adapter.resource, iface)
+ self.assertEqual(adapter.request, iface)
+
+ def test_add_resource_url_default_interfaces_mean_Interface(self):
+ from pyramid.interfaces import IResourceURL
+ config = self._makeOne(autocommit=True)
+ config.add_resource_url_adapter(DummyResourceURL)
+ iface = DummyIface()
+ adapter = config.registry.getMultiAdapter((iface, iface),
+ IResourceURL)
+ self.assertEqual(adapter.__class__, DummyResourceURL)
+ self.assertEqual(adapter.resource, iface)
+ self.assertEqual(adapter.request, iface)
+
+ def test_add_resource_url_nodefault_interfaces(self):
+ from zope.interface import Interface
+ from pyramid.interfaces import IResourceURL
+ config = self._makeOne(autocommit=True)
+ config.add_resource_url_adapter(DummyResourceURL, DummyIface,
+ DummyIface)
+ iface = DummyIface()
+ adapter = config.registry.getMultiAdapter((iface, iface),
+ IResourceURL)
+ self.assertEqual(adapter.__class__, DummyResourceURL)
+ self.assertEqual(adapter.resource, iface)
+ self.assertEqual(adapter.request, iface)
+ bad_result = config.registry.queryMultiAdapter(
+ (Interface, Interface),
+ IResourceURL,
+ )
+ self.assertEqual(bad_result, None)
+
+ def test_add_resource_url_adapter_introspectables(self):
+ config = self._makeOne()
+ config.add_resource_url_adapter(DummyResourceURL, DummyIface)
+ actions = config.action_state.actions
+ self.assertEqual(len(actions), 1)
+ intrs = actions[0]['introspectables']
+ self.assertEqual(len(intrs), 1)
+ intr = intrs[0]
+ self.assertEqual(intr.type_name, 'resource url adapter')
+ self.assertEqual(intr.discriminator,
+ ('resource url adapter', DummyIface, None))
+ self.assertEqual(intr.category_name, 'resource url adapters')
+ self.assertEqual(
+ intr.title,
+ "resource url adapter for resource iface "
+ "<class 'pyramid.tests.test_config.test_factories.DummyIface'>, "
+ "request_iface None"
+ )
+
+
class DummyRequest(object):
callables = None
@@ -186,3 +250,9 @@ class DummyTraverser(object):
class DummyIface(object):
pass
+
+class DummyResourceURL(object):
+ def __init__(self, resource, request):
+ self.resource = resource
+ self.request = request
+
diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py
index 10cda96d8..8a5215a2b 100644
--- a/pyramid/tests/test_request.py
+++ b/pyramid/tests/test_request.py
@@ -21,17 +21,16 @@ class TestRequest(unittest.TestCase):
from pyramid.request import Request
return Request
- def _registerContextURL(self):
- from pyramid.interfaces import IContextURL
+ def _registerResourceURL(self):
+ from pyramid.interfaces import IResourceURL
from zope.interface import Interface
- class DummyContextURL(object):
+ class DummyResourceURL(object):
def __init__(self, context, request):
- pass
- def __call__(self):
- return 'http://example.com/context/'
+ self.physical_path = '/context/'
+ self.virtual_path = '/context/'
self.config.registry.registerAdapter(
- DummyContextURL, (Interface, Interface),
- IContextURL)
+ DummyResourceURL, (Interface, Interface),
+ IResourceURL)
def test_charset_defaults_to_utf8(self):
r = self._makeOne({'PATH_INFO':'/'})
@@ -151,8 +150,14 @@ class TestRequest(unittest.TestCase):
self.assertEqual(inst.finished_callbacks, [])
def test_resource_url(self):
- self._registerContextURL()
- inst = self._makeOne({})
+ self._registerResourceURL()
+ environ = {
+ 'PATH_INFO':'/',
+ 'SERVER_NAME':'example.com',
+ 'SERVER_PORT':'80',
+ 'wsgi.url_scheme':'http',
+ }
+ inst = self._makeOne(environ)
root = DummyContext()
result = inst.resource_url(root)
self.assertEqual(result, 'http://example.com/context/')
diff --git a/pyramid/tests/test_url.py b/pyramid/tests/test_url.py
index 4c39d8e9c..3c36363ed 100644
--- a/pyramid/tests/test_url.py
+++ b/pyramid/tests/test_url.py
@@ -1,4 +1,5 @@
import unittest
+import warnings
from pyramid.testing import setUp
from pyramid.testing import tearDown
@@ -12,11 +13,16 @@ class TestURLMethodsMixin(unittest.TestCase):
def tearDown(self):
tearDown()
- def _makeOne(self):
+ def _makeOne(self, environ=None):
from pyramid.url import URLMethodsMixin
+ if environ is None:
+ environ = {}
class Request(URLMethodsMixin):
application_url = 'http://example.com:5432'
- request = Request()
+ script_name = ''
+ def __init__(self, environ):
+ self.environ = environ
+ request = Request(environ)
request.registry = self.config.registry
return request
@@ -31,114 +37,124 @@ class TestURLMethodsMixin(unittest.TestCase):
reg.registerAdapter(DummyContextURL, (Interface, Interface),
IContextURL)
+ def _registerResourceURL(self, reg):
+ 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/'
+ reg.registerAdapter(DummyResourceURL, (Interface, Interface),
+ IResourceURL)
+
def test_resource_url_root_default(self):
request = self._makeOne()
- self._registerContextURL(request.registry)
+ self._registerResourceURL(request.registry)
root = DummyContext()
result = request.resource_url(root)
- self.assertEqual(result, 'http://example.com/context/')
+ self.assertEqual(result, 'http://example.com:5432/context/')
def test_resource_url_extra_args(self):
request = self._makeOne()
- self._registerContextURL(request.registry)
+ self._registerResourceURL(request.registry)
context = DummyContext()
result = request.resource_url(context, 'this/theotherthing', 'that')
self.assertEqual(
result,
- 'http://example.com/context/this%2Ftheotherthing/that')
+ 'http://example.com:5432/context/this%2Ftheotherthing/that')
def test_resource_url_unicode_in_element_names(self):
request = self._makeOne()
- self._registerContextURL(request.registry)
+ self._registerResourceURL(request.registry)
uc = text_(b'La Pe\xc3\xb1a', 'utf-8')
context = DummyContext()
result = request.resource_url(context, uc)
self.assertEqual(result,
- 'http://example.com/context/La%20Pe%C3%B1a')
+ 'http://example.com:5432/context/La%20Pe%C3%B1a')
def test_resource_url_at_sign_in_element_names(self):
request = self._makeOne()
- self._registerContextURL(request.registry)
+ self._registerResourceURL(request.registry)
context = DummyContext()
result = request.resource_url(context, '@@myview')
self.assertEqual(result,
- 'http://example.com/context/@@myview')
+ 'http://example.com:5432/context/@@myview')
def test_resource_url_element_names_url_quoted(self):
request = self._makeOne()
- self._registerContextURL(request.registry)
+ self._registerResourceURL(request.registry)
context = DummyContext()
result = request.resource_url(context, 'a b c')
- self.assertEqual(result, 'http://example.com/context/a%20b%20c')
+ self.assertEqual(result, 'http://example.com:5432/context/a%20b%20c')
def test_resource_url_with_query_dict(self):
request = self._makeOne()
- self._registerContextURL(request.registry)
+ self._registerResourceURL(request.registry)
context = DummyContext()
uc = text_(b'La Pe\xc3\xb1a', 'utf-8')
result = request.resource_url(context, 'a', query={'a':uc})
self.assertEqual(result,
- 'http://example.com/context/a?a=La+Pe%C3%B1a')
+ 'http://example.com:5432/context/a?a=La+Pe%C3%B1a')
def test_resource_url_with_query_seq(self):
request = self._makeOne()
- self._registerContextURL(request.registry)
+ self._registerResourceURL(request.registry)
context = DummyContext()
uc = text_(b'La Pe\xc3\xb1a', 'utf-8')
result = request.resource_url(context, 'a', query=[('a', 'hi there'),
('b', uc)])
self.assertEqual(result,
- 'http://example.com/context/a?a=hi+there&b=La+Pe%C3%B1a')
+ 'http://example.com:5432/context/a?a=hi+there&b=La+Pe%C3%B1a')
def test_resource_url_anchor_is_after_root_when_no_elements(self):
request = self._makeOne()
- self._registerContextURL(request.registry)
+ self._registerResourceURL(request.registry)
context = DummyContext()
result = request.resource_url(context, anchor='a')
self.assertEqual(result,
- 'http://example.com/context/#a')
+ 'http://example.com:5432/context/#a')
def test_resource_url_anchor_is_after_elements_when_no_qs(self):
request = self._makeOne()
- self._registerContextURL(request.registry)
+ self._registerResourceURL(request.registry)
context = DummyContext()
result = request.resource_url(context, 'a', anchor='b')
self.assertEqual(result,
- 'http://example.com/context/a#b')
+ 'http://example.com:5432/context/a#b')
def test_resource_url_anchor_is_after_qs_when_qs_is_present(self):
request = self._makeOne()
- self._registerContextURL(request.registry)
+ self._registerResourceURL(request.registry)
context = DummyContext()
result = request.resource_url(context, 'a',
query={'b':'c'}, anchor='d')
self.assertEqual(result,
- 'http://example.com/context/a?b=c#d')
+ 'http://example.com:5432/context/a?b=c#d')
def test_resource_url_anchor_is_encoded_utf8_if_unicode(self):
request = self._makeOne()
- self._registerContextURL(request.registry)
+ self._registerResourceURL(request.registry)
context = DummyContext()
uc = text_(b'La Pe\xc3\xb1a', 'utf-8')
result = request.resource_url(context, anchor=uc)
self.assertEqual(
result,
native_(
- text_(b'http://example.com/context/#La Pe\xc3\xb1a',
+ text_(b'http://example.com:5432/context/#La Pe\xc3\xb1a',
'utf-8'),
'utf-8')
)
def test_resource_url_anchor_is_not_urlencoded(self):
request = self._makeOne()
- self._registerContextURL(request.registry)
+ self._registerResourceURL(request.registry)
context = DummyContext()
result = request.resource_url(context, anchor=' /#')
self.assertEqual(result,
- 'http://example.com/context/# /#')
+ 'http://example.com:5432/context/# /#')
- def test_resource_url_no_IContextURL_registered(self):
- # falls back to TraversalContextURL
+ def test_resource_url_no_IResourceURL_registered(self):
+ # falls back to ResourceURL
root = DummyContext()
root.__name__ = ''
root.__parent__ = None
@@ -149,12 +165,98 @@ class TestURLMethodsMixin(unittest.TestCase):
def test_resource_url_no_registry_on_request(self):
request = self._makeOne()
- self._registerContextURL(request.registry)
+ self._registerResourceURL(request.registry)
del request.registry
root = DummyContext()
result = request.resource_url(root)
+ self.assertEqual(result, 'http://example.com:5432/context/')
+
+ def test_resource_url_finds_IContextURL(self):
+ request = self._makeOne()
+ self._registerContextURL(request.registry)
+ root = DummyContext()
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter('always')
+ result = request.resource_url(root)
+ self.assertEqual(len(w), 1)
self.assertEqual(result, 'http://example.com/context/')
+
+ def test_resource_url_with_app_url(self):
+ request = self._makeOne()
+ self._registerResourceURL(request.registry)
+ root = DummyContext()
+ result = request.resource_url(root, app_url='http://somewhere.com')
+ self.assertEqual(result, 'http://somewhere.com/context/')
+
+ def test_resource_url_with_scheme(self):
+ environ = {
+ 'wsgi.url_scheme':'http',
+ 'SERVER_PORT':'8080',
+ 'SERVER_NAME':'example.com',
+ }
+ request = self._makeOne(environ)
+ self._registerResourceURL(request.registry)
+ root = DummyContext()
+ result = request.resource_url(root, scheme='https')
+ self.assertEqual(result, 'https://example.com/context/')
+
+ def test_resource_url_with_host(self):
+ environ = {
+ 'wsgi.url_scheme':'http',
+ 'SERVER_PORT':'8080',
+ 'SERVER_NAME':'example.com',
+ }
+ request = self._makeOne(environ)
+ self._registerResourceURL(request.registry)
+ root = DummyContext()
+ result = request.resource_url(root, host='someotherhost.com')
+ self.assertEqual(result, 'http://someotherhost.com:8080/context/')
+
+ def test_resource_url_with_port(self):
+ environ = {
+ 'wsgi.url_scheme':'http',
+ 'SERVER_PORT':'8080',
+ 'SERVER_NAME':'example.com',
+ }
+ request = self._makeOne(environ)
+ self._registerResourceURL(request.registry)
+ root = DummyContext()
+ result = request.resource_url(root, port='8181')
+ self.assertEqual(result, 'http://example.com:8181/context/')
+
+ def test_resource_url_with_local_url(self):
+ environ = {
+ 'wsgi.url_scheme':'http',
+ 'SERVER_PORT':'8080',
+ 'SERVER_NAME':'example.com',
+ }
+ request = self._makeOne(environ)
+ self._registerResourceURL(request.registry)
+ root = DummyContext()
+ def resource_url(req, info):
+ self.assertEqual(req, request)
+ self.assertEqual(info['virtual_path'], '/context/')
+ self.assertEqual(info['physical_path'], '/context/')
+ self.assertEqual(info['app_url'], 'http://example.com:5432')
+ return 'http://example.com/contextabc/'
+ root.__resource_url__ = resource_url
+ result = request.resource_url(root)
+ self.assertEqual(result, 'http://example.com/contextabc/')
+
+ def test_resource_path(self):
+ request = self._makeOne()
+ self._registerResourceURL(request.registry)
+ root = DummyContext()
+ result = request.resource_path(root)
+ self.assertEqual(result, '/context/')
+ def test_resource_path_kwarg(self):
+ request = self._makeOne()
+ self._registerResourceURL(request.registry)
+ root = DummyContext()
+ result = request.resource_path(root, anchor='abc')
+ self.assertEqual(result, '/context/#abc')
+
def test_route_url_with_elements(self):
from pyramid.interfaces import IRoutesMapper
request = self._makeOne()
@@ -234,6 +336,47 @@ class TestURLMethodsMixin(unittest.TestCase):
self.assertEqual(result,
'http://example2.com/1/2/3')
+ def test_route_url_with_host(self):
+ from pyramid.interfaces import IRoutesMapper
+ environ = {
+ 'wsgi.url_scheme':'http',
+ 'SERVER_PORT':'5432',
+ }
+ request = self._makeOne(environ)
+ mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3'))
+ request.registry.registerUtility(mapper, IRoutesMapper)
+ result = request.route_url('flub', _host='someotherhost.com')
+ self.assertEqual(result,
+ 'http://someotherhost.com:5432/1/2/3')
+
+ def test_route_url_with_port(self):
+ from pyramid.interfaces import IRoutesMapper
+ environ = {
+ 'wsgi.url_scheme':'http',
+ 'SERVER_PORT':'5432',
+ 'SERVER_NAME':'example.com',
+ }
+ request = self._makeOne(environ)
+ mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3'))
+ request.registry.registerUtility(mapper, IRoutesMapper)
+ result = request.route_url('flub', _port='8080')
+ self.assertEqual(result,
+ 'http://example.com:8080/1/2/3')
+
+ def test_route_url_with_scheme(self):
+ from pyramid.interfaces import IRoutesMapper
+ environ = {
+ 'wsgi.url_scheme':'http',
+ 'SERVER_PORT':'5432',
+ 'SERVER_NAME':'example.com',
+ }
+ request = self._makeOne(environ)
+ mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3'))
+ request.registry.registerUtility(mapper, IRoutesMapper)
+ result = request.route_url('flub', _scheme='https')
+ self.assertEqual(result,
+ 'https://example.com/1/2/3')
+
def test_route_url_generation_error(self):
from pyramid.interfaces import IRoutesMapper
request = self._makeOne()
@@ -471,6 +614,168 @@ class TestURLMethodsMixin(unittest.TestCase):
{'_app_url':'/foo'})
)
+ def test_partial_application_url_with_http_host_default_port_http(self):
+ environ = {
+ 'wsgi.url_scheme':'http',
+ 'HTTP_HOST':'example.com:80',
+ }
+ request = self._makeOne(environ)
+ result = request.partial_application_url()
+ self.assertEqual(result, 'http://example.com')
+
+ def test_partial_application_url_with_http_host_default_port_https(self):
+ environ = {
+ 'wsgi.url_scheme':'https',
+ 'HTTP_HOST':'example.com:443',
+ }
+ request = self._makeOne(environ)
+ result = request.partial_application_url()
+ self.assertEqual(result, 'https://example.com')
+
+ def test_partial_application_url_with_http_host_nondefault_port_http(self):
+ environ = {
+ 'wsgi.url_scheme':'http',
+ 'HTTP_HOST':'example.com:8080',
+ }
+ request = self._makeOne(environ)
+ result = request.partial_application_url()
+ self.assertEqual(result, 'http://example.com:8080')
+
+ def test_partial_application_url_with_http_host_nondefault_port_https(self):
+ environ = {
+ 'wsgi.url_scheme':'https',
+ 'HTTP_HOST':'example.com:4443',
+ }
+ request = self._makeOne(environ)
+ result = request.partial_application_url()
+ self.assertEqual(result, 'https://example.com:4443')
+
+ def test_partial_application_url_with_http_host_no_colon(self):
+ environ = {
+ 'wsgi.url_scheme':'http',
+ 'HTTP_HOST':'example.com',
+ 'SERVER_PORT':'80',
+ }
+ request = self._makeOne(environ)
+ result = request.partial_application_url()
+ self.assertEqual(result, 'http://example.com')
+
+ def test_partial_application_url_no_http_host(self):
+ environ = {
+ 'wsgi.url_scheme':'http',
+ 'SERVER_NAME':'example.com',
+ 'SERVER_PORT':'80',
+ }
+ request = self._makeOne(environ)
+ result = request.partial_application_url()
+ self.assertEqual(result, 'http://example.com')
+
+ def test_partial_application_replace_port(self):
+ environ = {
+ 'wsgi.url_scheme':'http',
+ 'SERVER_NAME':'example.com',
+ 'SERVER_PORT':'80',
+ }
+ request = self._makeOne(environ)
+ result = request.partial_application_url(port=8080)
+ self.assertEqual(result, 'http://example.com:8080')
+
+ def test_partial_application_replace_scheme_https_special_case(self):
+ environ = {
+ 'wsgi.url_scheme':'http',
+ 'SERVER_NAME':'example.com',
+ 'SERVER_PORT':'80',
+ }
+ request = self._makeOne(environ)
+ result = request.partial_application_url(scheme='https')
+ self.assertEqual(result, 'https://example.com')
+
+ def test_partial_application_replace_scheme_https_special_case_avoid(self):
+ environ = {
+ 'wsgi.url_scheme':'http',
+ 'SERVER_NAME':'example.com',
+ 'SERVER_PORT':'80',
+ }
+ request = self._makeOne(environ)
+ result = request.partial_application_url(scheme='https', port='8080')
+ self.assertEqual(result, 'https://example.com:8080')
+
+ def test_partial_application_replace_scheme_http_special_case(self):
+ environ = {
+ 'wsgi.url_scheme':'https',
+ 'SERVER_NAME':'example.com',
+ 'SERVER_PORT':'8080',
+ }
+ request = self._makeOne(environ)
+ result = request.partial_application_url(scheme='http')
+ self.assertEqual(result, 'http://example.com')
+
+ def test_partial_application_replace_scheme_http_special_case_avoid(self):
+ environ = {
+ 'wsgi.url_scheme':'https',
+ 'SERVER_NAME':'example.com',
+ 'SERVER_PORT':'8000',
+ }
+ request = self._makeOne(environ)
+ result = request.partial_application_url(scheme='http', port='8080')
+ self.assertEqual(result, 'http://example.com:8080')
+
+ def test_partial_application_replace_host_no_port(self):
+ environ = {
+ 'wsgi.url_scheme':'http',
+ 'SERVER_NAME':'example.com',
+ 'SERVER_PORT':'80',
+ }
+ request = self._makeOne(environ)
+ result = request.partial_application_url(host='someotherhost.com')
+ self.assertEqual(result, 'http://someotherhost.com')
+
+ def test_partial_application_replace_host_with_port(self):
+ environ = {
+ 'wsgi.url_scheme':'http',
+ 'SERVER_NAME':'example.com',
+ 'SERVER_PORT':'8000',
+ }
+ request = self._makeOne(environ)
+ result = request.partial_application_url(host='someotherhost.com:8080')
+ self.assertEqual(result, 'http://someotherhost.com:8080')
+
+ def test_partial_application_replace_host_and_port(self):
+ environ = {
+ 'wsgi.url_scheme':'http',
+ 'SERVER_NAME':'example.com',
+ 'SERVER_PORT':'80',
+ }
+ request = self._makeOne(environ)
+ result = request.partial_application_url(host='someotherhost.com:8080',
+ port='8000')
+ self.assertEqual(result, 'http://someotherhost.com:8000')
+
+ def test_partial_application_replace_host_port_and_scheme(self):
+ environ = {
+ 'wsgi.url_scheme':'http',
+ 'SERVER_NAME':'example.com',
+ 'SERVER_PORT':'80',
+ }
+ request = self._makeOne(environ)
+ result = request.partial_application_url(
+ host='someotherhost.com:8080',
+ port='8000',
+ scheme='https',
+ )
+ self.assertEqual(result, 'https://someotherhost.com:8000')
+
+ def test_partial_application_url_with_custom_script_name(self):
+ environ = {
+ 'wsgi.url_scheme':'http',
+ 'SERVER_NAME':'example.com',
+ 'SERVER_PORT':'8000',
+ }
+ request = self._makeOne(environ)
+ request.script_name = '/abc'
+ result = request.partial_application_url()
+ self.assertEqual(result, 'http://example.com:8000/abc')
+
class Test_route_url(unittest.TestCase):
def _callFUT(self, route_name, request, *elements, **kw):
from pyramid.url import route_url
diff --git a/pyramid/traversal.py b/pyramid/traversal.py
index 84dcd33ec..9801f8f18 100644
--- a/pyramid/traversal.py
+++ b/pyramid/traversal.py
@@ -6,6 +6,7 @@ from zope.interface.interfaces import IInterface
from repoze.lru import lru_cache
from pyramid.interfaces import (
+ IResourceURL,
IContextURL,
IRequestFactory,
ITraverser,
@@ -730,17 +731,33 @@ class ResourceTreeTraverser(object):
ModelGraphTraverser = ResourceTreeTraverser # b/w compat, not API, used in wild
-@implementer(IContextURL)
-class TraversalContextURL(object):
- """ The IContextURL adapter used to generate URLs for a resource in a
- resource tree"""
-
+@implementer(IResourceURL, IContextURL)
+class ResourceURL(object):
vroot_varname = VH_ROOT_KEY
- def __init__(self, context, request):
- self.context = context
+ def __init__(self, resource, request):
+ physical_path = resource_path(resource)
+ if physical_path != '/':
+ physical_path = physical_path + '/'
+
+ virtual_path = physical_path
+
+ environ = request.environ
+ vroot_path = environ.get(self.vroot_varname)
+
+ # if the physical path starts with the virtual root path, trim it out
+ # of the virtual path
+ if vroot_path is not None:
+ if physical_path.startswith(vroot_path):
+ virtual_path = physical_path[len(vroot_path):]
+
+ self.virtual_path = virtual_path
+ self.physical_path = physical_path
+ self.resource = resource
+ self.context = resource # bw compat alias for IContextURL compat
self.request = request
+ # IContextURL method (deprecated in 1.3)
def virtual_root(self):
environ = self.request.environ
vroot_varname = self.vroot_varname
@@ -753,6 +770,7 @@ class TraversalContextURL(object):
except AttributeError:
return find_root(self.context)
+ # IContextURL method (deprecated in 1.3)
def __call__(self):
""" Generate a URL based on the :term:`lineage` of a :term:`resource`
object that is ``self.context``. If any resource in the context
@@ -762,35 +780,21 @@ class TraversalContextURL(object):
'virtual root path': the path of the URL generated by this will be
left-stripped of this virtual root path value.
"""
- resource = self.context
- physical_path = resource_path(resource)
- if physical_path != '/':
- physical_path = physical_path + '/'
- virtual_path = physical_path
-
- request = self.request
- environ = request.environ
- vroot_varname = self.vroot_varname
- vroot_path = environ.get(vroot_varname)
-
- # if the physical path starts with the virtual root path, trim it out
- # of the virtual path
- if vroot_path is not None:
- if physical_path.startswith(vroot_path):
- virtual_path = physical_path[len(vroot_path):]
-
- local_url = getattr(resource, '__resource_url__', None)
+ local_url = getattr(self.context, '__resource_url__', None)
if local_url is not None:
- result = local_url(request,
- {'virtual_path':virtual_path,
- 'physical_path':physical_path},
- )
+ result = local_url(
+ self.request,
+ {'virtual_path':self.virtual_path,
+ 'physical_path':self.physical_path},
+ )
if result is not None:
# allow it to punt by returning ``None``
return result
- app_url = request.application_url # never ends in a slash
- return app_url + virtual_path
+ app_url = self.request.application_url # never ends in a slash
+ return app_url + self.virtual_path
+
+TraversalContextURL = ResourceURL # bw compat as of 1.3
@lru_cache(1000)
def _join_path_tuple(tuple):
diff --git a/pyramid/url.py b/pyramid/url.py
index e6a508c17..d1c1b6f42 100644
--- a/pyramid/url.py
+++ b/pyramid/url.py
@@ -1,32 +1,87 @@
""" Utility functions for dealing with URLs in pyramid """
import os
+import warnings
from repoze.lru import lru_cache
from pyramid.interfaces import (
- IContextURL,
+ IResourceURL,
IRoutesMapper,
IStaticURLInfo,
)
from pyramid.compat import (
native_,
+ bytes_,
text_type,
+ url_quote,
)
from pyramid.encode import urlencode
from pyramid.path import caller_package
from pyramid.threadlocal import get_current_registry
from pyramid.traversal import (
- TraversalContextURL,
+ ResourceURL,
quote_path_segment,
)
+PATH_SAFE = '/:@&+$,' # from webob
+
class URLMethodsMixin(object):
""" Request methods mixin for BaseRequest having to do with URL
generation """
+ def partial_application_url(self, scheme=None, host=None, port=None):
+ """
+ Construct the URL defined by request.application_url, replacing any
+ of the default scheme, host, or port portions with user-supplied
+ variants.
+
+ If ``scheme`` is passed as ``https``, and the ``port`` is *not*
+ passed, the ``port`` value is assumed to ``443``. Likewise, if
+ ``scheme`` is passed as ``http`` and ``port`` is not passed, the
+ ``port`` value is assumed to be ``80``.
+ """
+ e = self.environ
+ if scheme is None:
+ scheme = e['wsgi.url_scheme']
+ else:
+ if scheme == 'https':
+ if port is None:
+ port = '443'
+ if scheme == 'http':
+ if port is None:
+ port = '80'
+ url = scheme + '://'
+ if port is not None:
+ port = str(port)
+ if host is None:
+ host = e.get('HTTP_HOST')
+ if host is None:
+ host = e['SERVER_NAME']
+ if port is None:
+ if ':' in host:
+ host, port = host.split(':', 1)
+ else:
+ port = e['SERVER_PORT']
+ else:
+ if ':' in host:
+ host, _ = host.split(':', 1)
+ if scheme == 'https':
+ if port == '443':
+ port = None
+ elif scheme == 'http':
+ if port == '80':
+ port = None
+ url += host
+ if port:
+ url += ':%s' % port
+
+ url_encoding = getattr(self, 'url_encoding', 'utf-8') # webob 1.2b3+
+ bscript_name = bytes_(self.script_name, url_encoding)
+ return url + url_quote(bscript_name, PATH_SAFE)
+
def route_url(self, route_name, *elements, **kw):
"""Generates a fully qualified URL for a named :app:`Pyramid`
:term:`route configuration`.
@@ -105,6 +160,15 @@ class URLMethodsMixin(object):
element will always follow the query element,
e.g. ``http://example.com?foo=1#bar``.
+ If any of ``_scheme``, ``_host``, or ``_port`` is passed and is
+ non-``None``, the provided value will replace the named portion in
+ the generated URL. If ``_scheme`` is passed as ``https``, and
+ ``_port`` is not passed, the ``_port`` value is assumed to have been
+ passed as ``443``. Likewise, if ``_scheme`` is passed as ``http``
+ and ``_port`` is not passed, the ``_port`` value is assumed to have
+ been passed as ``80``. To avoid this behavior, always explicitly pass
+ ``_port`` whenever you pass ``_scheme``.
+
If a keyword ``_app_url`` is present, it will be used as the
protocol/hostname/port/leading path prefix of the generated URL.
For example, using an ``_app_url`` of
@@ -116,6 +180,10 @@ class URLMethodsMixin(object):
``request.application_url`` will be used as the prefix (the
default).
+ If both ``_app_url`` and any of ``_scheme``, ``_host``, or ``_port``
+ are passed, ``_app_url`` takes precedence and any values passed for
+ ``_scheme``, ``_host``, and ``_port`` will be ignored.
+
This function raises a :exc:`KeyError` if the URL cannot be
generated due to missing replacement names. Extra replacement
names are ignored.
@@ -140,6 +208,9 @@ class URLMethodsMixin(object):
anchor = ''
qs = ''
app_url = None
+ host = None
+ scheme = None
+ port = None
if '_query' in kw:
qs = '?' + urlencode(kw.pop('_query'), doseq=True)
@@ -152,6 +223,21 @@ class URLMethodsMixin(object):
if '_app_url' in kw:
app_url = kw.pop('_app_url')
+ if '_host' in kw:
+ host = kw.pop('_host')
+
+ if '_scheme' in kw:
+ scheme = kw.pop('_scheme')
+
+ if '_port' in kw:
+ port = kw.pop('_port')
+
+ if app_url is None:
+ if (scheme is not None or host is not None or port is not None):
+ app_url = self.partial_application_url(scheme, host, port)
+ else:
+ app_url = self.application_url
+
path = route.generate(kw) # raises KeyError if generate fails
if elements:
@@ -161,12 +247,6 @@ class URLMethodsMixin(object):
else:
suffix = ''
- if app_url is None:
- # we only defer lookup of application_url until here because
- # it's somewhat expensive; we won't need to do it if we've
- # been passed _app_url
- app_url = self.application_url
-
return app_url + path + suffix + qs + anchor
def route_path(self, route_name, *elements, **kw):
@@ -206,7 +286,7 @@ class URLMethodsMixin(object):
:term:`resource` object based on the ``wsgi.url_scheme``,
``HTTP_HOST`` or ``SERVER_NAME`` in the request, plus any
``SCRIPT_NAME``. The overall result of this method is always a
- UTF-8 encoded string (never Unicode).
+ UTF-8 encoded string.
Examples::
@@ -226,6 +306,10 @@ class URLMethodsMixin(object):
http://example.com/a.html#abc
+ request.resource_url(resource, app_url='') =>
+
+ /
+
Any positional arguments passed in as ``elements`` must be strings
Unicode objects, or integer objects. These will be joined by slashes
and appended to the generated resource URL. Each of the elements
@@ -275,6 +359,38 @@ class URLMethodsMixin(object):
will always follow the query element,
e.g. ``http://example.com?foo=1#bar``.
+ If any of the keyword arguments ``scheme``, ``host``, or ``port`` is
+ passed and is non-``None``, the provided value will replace the named
+ portion in the generated URL. For example, if you pass
+ ``scheme='https'``, and the URL that would be generated without the
+ scheme replacement is ``http://foo.com``, the result will be
+ ``https://foo.com``.
+
+ If ``scheme`` is passed as ``https``, and an explicit ``port`` is not
+ passed, the ``port`` value is assumed to have been passed as ``443``.
+ Likewise, if ``scheme`` is passed as ``http`` and ``port`` is not
+ passed, the ``port`` value is assumed to have been passed as
+ ``80``. To avoid this behavior, always explicitly pass ``port``
+ whenever you pass ``scheme``.
+
+ If a keyword argument ``app_url`` is passed and is not ``None``, it
+ should be a string that will be used as the port/hostname/initial
+ path portion of the generated URL instead of the default request
+ application URL. For example, if ``app_url='http://foo'``, then the
+ resulting url of a resource that has a path of ``/baz/bar`` will be
+ ``http://foo/baz/bar``. If you want to generate completely relative
+ URLs with no leading scheme, host, port, or initial path, you can
+ pass ``app_url=''`. Passing ``app_url=''` when the resource path is
+ ``/baz/bar`` will return ``/baz/bar``.
+
+ .. note::
+
+ ``app_url`` is new as of Pyramid 1.3.
+
+ If ``app_url`` is passed and any of ``scheme``, ``port``, or ``host``
+ 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) that for
the base resource which is operated upon by this function. See also
@@ -305,10 +421,69 @@ class URLMethodsMixin(object):
except AttributeError:
reg = get_current_registry() # b/c
- context_url = reg.queryMultiAdapter((resource, self), IContextURL)
- if context_url is None:
- context_url = TraversalContextURL(resource, self)
- resource_url = context_url()
+ url_adapter = reg.queryMultiAdapter((resource, self), IResourceURL)
+ if url_adapter is None:
+ url_adapter = ResourceURL(resource, self)
+
+ virtual_path = getattr(url_adapter, 'virtual_path', None)
+
+ if virtual_path is None:
+ # old-style IContextURL adapter (Pyramid 1.2 and previous)
+ warnings.warn(
+ 'Pyramid is using an IContextURL adapter to generate a '
+ 'resource URL; any "app_url", "host", "port", or "scheme" '
+ 'arguments passed to resource_url are being ignored. To '
+ 'avoid this behavior, as of Pyramid 1.3, register an '
+ 'IResourceURL adapter instead of an IContextURL '
+ 'adapter for the resource type(s). IContextURL adapters '
+ 'will be ignored in a later major release of Pyramid.',
+ DeprecationWarning,
+ 2)
+
+ resource_url = url_adapter()
+
+ else:
+ # newer-style IResourceURL adapter (Pyramid 1.3 and after)
+ app_url = None
+ scheme = None
+ host = None
+ port = None
+
+ if 'app_url' in kw:
+ app_url = kw['app_url']
+
+ if 'scheme' in kw:
+ scheme = kw['scheme']
+
+ if 'host' in kw:
+ host = kw['host']
+
+ if 'port' in kw:
+ port = kw['port']
+
+ if app_url is None:
+ if scheme or host or port:
+ app_url = self.partial_application_url(scheme, host, port)
+ else:
+ app_url = self.application_url
+
+ resource_url = None
+ local_url = getattr(resource, '__resource_url__', None)
+
+ if local_url is not None:
+ # the resource handles its own url generation
+ d = dict(
+ virtual_path = virtual_path,
+ physical_path = url_adapter.physical_path,
+ app_url = app_url,
+ )
+ # allow __resource_url__ to punt by returning None
+ resource_url = local_url(self, d)
+
+ if resource_url is None:
+ # the resource did not handle its own url generation or the
+ # __resource_url__ function returned None
+ resource_url = app_url + virtual_path
qs = ''
anchor = ''
@@ -331,6 +506,31 @@ class URLMethodsMixin(object):
model_url = resource_url # b/w compat forever
+ def resource_path(self, resource, *elements, **kw):
+ """
+ Generates a path (aka a 'relative URL', a URL minus the host, scheme,
+ and port) for a :term:`resource`.
+
+ This function accepts the same argument as
+ :meth:`pyramid.request.Request.resource_url` and performs the same
+ duty. It just omits the host, port, and scheme information in the
+ return value; only the script_name, path, query parameters, and
+ anchor data are present in the returned string.
+
+ .. note::
+
+ Calling ``request.resource_path(resource)`` is the same as calling
+ ``request.resource_path(resource, app_url=request.script_name)``.
+ :meth:`pyramid.request.Request.resource_path` is, in fact,
+ implemented in terms of
+ :meth:`pyramid.request.Request.resource_url` in just this way. As
+ a result, any ``app_url`` passed within the ``**kw`` values to
+ ``route_path`` will be ignored. ``scheme``, ``host``, and
+ ``port`` are also ignored.
+ """
+ kw['app_url'] = self.script_name
+ return self.resource_url(resource, *elements, **kw)
+
def static_url(self, path, **kw):
"""
Generates a fully qualified URL for a static :term:`asset`.