summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2012-02-17 01:08:42 -0500
committerChris McDonough <chrism@plope.com>2012-02-17 01:08:42 -0500
commitc51896756eeffc7e8c50ad71300ec355ae47465a (patch)
treec988f5d6cd7d2798b20935c7e614441efac89bf8
parentb2ea4c88b8b3bc9ed657160d8a888780d6c41844 (diff)
downloadpyramid-c51896756eeffc7e8c50ad71300ec355ae47465a.tar.gz
pyramid-c51896756eeffc7e8c50ad71300ec355ae47465a.tar.bz2
pyramid-c51896756eeffc7e8c50ad71300ec355ae47465a.zip
Features
-------- - 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. - 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. Misc ---- - Change ``set_traverser`` API name to ``add_traverser``. Ref #438.
-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`.