diff options
| -rw-r--r-- | CHANGES.txt | 54 | ||||
| -rw-r--r-- | docs/api/config.rst | 2 | ||||
| -rw-r--r-- | docs/api/interfaces.rst | 4 | ||||
| -rw-r--r-- | docs/api/request.rst | 2 | ||||
| -rw-r--r-- | docs/narr/hooks.rst | 59 | ||||
| -rw-r--r-- | docs/whatsnew-1.3.rst | 56 | ||||
| -rw-r--r-- | pyramid/config/factories.py | 77 | ||||
| -rw-r--r-- | pyramid/interfaces.py | 43 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_factories.py | 86 | ||||
| -rw-r--r-- | pyramid/tests/test_request.py | 25 | ||||
| -rw-r--r-- | pyramid/tests/test_url.py | 363 | ||||
| -rw-r--r-- | pyramid/traversal.py | 66 | ||||
| -rw-r--r-- | pyramid/url.py | 226 |
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`. |
