summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2012-02-17 18:29:44 -0500
committerChris McDonough <chrism@plope.com>2012-02-17 18:29:44 -0500
commit222638ad3760692ac0ec5368db4baaee9272e818 (patch)
treeb8fc997119a2b470db290c8270241e8991eb797e
parentcdb5f5a395c53e18a250651b6c9c3e0322b0dfe5 (diff)
parent305d23f9e9dd095f4fdface116a2155bd86a453c (diff)
downloadpyramid-222638ad3760692ac0ec5368db4baaee9272e818.tar.gz
pyramid-222638ad3760692ac0ec5368db4baaee9272e818.tar.bz2
pyramid-222638ad3760692ac0ec5368db4baaee9272e818.zip
Merge branch '1.3-branch'
-rw-r--r--CHANGES.txt76
-rw-r--r--TODO.txt30
-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.rst88
-rw-r--r--docs/narr/introspector.rst15
-rw-r--r--docs/narr/project.rst19
-rw-r--r--docs/narr/renderers.rst20
-rw-r--r--docs/narr/resources.rst39
-rw-r--r--docs/narr/traversal.rst9
-rw-r--r--docs/tutorials/.gitignore1
-rw-r--r--docs/tutorials/wiki2/authorization.rst4
-rw-r--r--docs/tutorials/wiki2/definingviews.rst8
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/views.py13
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/views.py13
-rw-r--r--docs/whatsnew-1.3.rst100
-rw-r--r--pyramid/config/__init__.py11
-rw-r--r--pyramid/config/factories.py143
-rw-r--r--pyramid/config/views.py15
-rw-r--r--pyramid/interfaces.py43
-rw-r--r--pyramid/renderers.py4
-rw-r--r--pyramid/scaffolds/__init__.py11
-rw-r--r--pyramid/tests/test_config/test_factories.py117
-rw-r--r--pyramid/tests/test_config/test_init.py20
-rw-r--r--pyramid/tests/test_renderers.py6
-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.py231
30 files changed, 1274 insertions, 224 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 411681d81..22f8320f9 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -17,6 +17,82 @@ Features
The error message now contains information about the view callable itself
as well as the result of calling it.
+- Better error message when a .pyc-only module is ``config.include`` -ed.
+ This is not permitted due to error reporting requirements, and a better
+ error message is shown when it is attempted. Previously it would fail with
+ something like "AttributeError: 'NoneType' object has no attribute
+ 'rfind'".
+
+- 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
+ purely a change to reduce the amount of typing required to use request
+ 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
+-------------
+
+- Don't create a ``session`` instance in SQLA Wiki tutorial, use raw
+ ``DBSession`` instead (this is more common in real SQLA apps).
+
Dependencies
------------
diff --git a/TODO.txt b/TODO.txt
index 3d11470dd..ab26a87a8 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -1,26 +1,22 @@
Pyramid TODOs
=============
-Must-Have
----------
-
-- Fix deployment recipes in cookbook (discourage proxying without changing
- server).
+Nice-to-Have
+------------
-- Use waitress instead of wsgiref.
+- Fix renderers chapter to better document system values passed to template
+ renderers.
-- pyramid.config.util.ActionInfo.__str__ potentially returns Unicode under
- Py2, fix.
+- Put includes in development.ini on separate lines and fix project.rst to
+ tell people to comment out only the debugtoolbar include when they want to
+ disable.
-- Tests for view names/route patterns that contain Unicode.
-
-Nice-to-Have
-------------
+- Modify view mapper narrative docs to not use pyramid_handlers.
- Modify the urldispatch chapter examples to assume a scan rather than
``add_view``.
-- Decorator for append_slash_notfound_view_factory?
+- Decorator for append_slash_notfound_view_factory.
- Introspection:
@@ -38,18 +34,10 @@ Nice-to-Have
- Fix deployment recipes in cookbook (discourage proxying without changing
server).
-Nice-to-Have
-------------
-
-- CherryPy server testing / exploded from CherryPy itself.
-
- Try "with transaction.manager" in an exception view with SQLA (preempt
homina homina response about how to write "to the database" from within in
an exception view).
-- Add a default-view-config-params decorator that can be applied to a class
- which names defaults for method-based view_config decorator options.
-
- Add narrative docs for wsgiapp and wsgiapp2.
- Flesh out "Paste" narrative docs chapter.
diff --git a/docs/api/config.rst b/docs/api/config.rst
index d16930cc0..3fc2cfc44 100644
--- a/docs/api/config.rst
+++ b/docs/api/config.rst
@@ -94,6 +94,8 @@
.. automethod:: set_notfound_view
+ .. automethod:: add_traverser
+
.. automethod:: set_renderer_globals_factory(factory)
.. attribute:: introspectable
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 fd6544416..2c4310080 100644
--- a/docs/narr/hooks.rst
+++ b/docs/narr/hooks.rst
@@ -406,11 +406,10 @@ via configuration.
.. code-block:: python
:linenos:
- from pyramid.interfaces import ITraverser
- from zope.interface import Interface
+ from pyramid.config import Configurator
from myapp.traversal import Traverser
-
- config.registry.registerAdapter(Traverser, (Interface,), ITraverser)
+ config = Configurator()
+ config.set_traverser(Traverser)
In the example above, ``myapp.traversal.Traverser`` is assumed to be a class
that implements the following interface:
@@ -456,12 +455,11 @@ used. Otherwise, the default traverser would be used. For example:
.. code-block:: python
:linenos:
- from pyramid.interfaces import ITraverser
- from zope.interface import Interface
from myapp.traversal import Traverser
from myapp.resources import MyRoot
-
- config.registry.registerAdapter(Traverser, (MyRoot,), ITraverser)
+ from pyramid.config import Configurator
+ config = Configurator()
+ config.set_traverser(Traverser, MyRoot)
If the above stanza was added to a Pyramid ``__init__.py`` file's ``main``
function, :app:`Pyramid` would use the ``myapp.traversal.Traverser`` only
@@ -481,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
@@ -606,24 +601,24 @@ adapter to the more complex IResponse interface:
If you want to implement your own Response object instead of using the
:class:`pyramid.response.Response` object in any capacity at all, you'll have
to make sure the object implements every attribute and method outlined in
-:class:`pyramid.interfaces.IResponse` and you'll have to ensure that it's
-marked up with ``zope.interface.implements(IResponse)``:
+:class:`pyramid.interfaces.IResponse` and you'll have to ensure that it uses
+``zope.interface.implementer(IResponse)`` as a class decoratoror.
.. code-block:: python
:linenos:
from pyramid.interfaces import IResponse
- from zope.interface import implements
+ from zope.interface import implementer
+ @implementer(IResponse)
class MyResponse(object):
- implements(IResponse)
# ... an implementation of every method and attribute
# documented in IResponse should follow ...
When an alternate response object implementation is returned by a view
callable, if that object asserts that it implements
:class:`~pyramid.interfaces.IResponse` (via
-``zope.interface.implements(IResponse)``) , an adapter needn't be registered
+``zope.interface.implementer(IResponse)``) , an adapter needn't be registered
for the object; Pyramid will use it directly.
An IResponse adapter for ``webob.Response`` (as opposed to
@@ -812,14 +807,15 @@ performed, enabling you to set up the utility in advance:
.. code-block:: python
:linenos:
+ from zope.interface import implementer
+
from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from mypackage.interfaces import IMyUtility
+ @implementer(IMyUtility)
class UtilityImplementation:
- implements(IMyUtility)
-
def __init__(self):
self.registrations = {}
diff --git a/docs/narr/introspector.rst b/docs/narr/introspector.rst
index 11d779854..08cc430f6 100644
--- a/docs/narr/introspector.rst
+++ b/docs/narr/introspector.rst
@@ -529,6 +529,21 @@ introspectables in categories not described here.
A normalized version of the ``spec`` argument provided to
``add_static_view``.
+``traversers``
+
+ Each introspectable in the ``traversers`` category represents a call to
+ :meth:`pyramid.config.Configurator.add_traverser`; each will have the
+ following data.
+
+ ``iface``
+
+ The (resolved) interface or class object that represents the return value
+ of a root factory that this traverser will be used for.
+
+ ``factory``
+
+ The (resolved) traverser class.
+
Introspection in the Toolbar
----------------------------
diff --git a/docs/narr/project.rst b/docs/narr/project.rst
index d69f0cf13..4566a4fb8 100644
--- a/docs/narr/project.rst
+++ b/docs/narr/project.rst
@@ -322,9 +322,26 @@ image again.
.. image:: project-debug.png
+If you don't see the debug toolbar image on the right hand top of the page,
+it means you're browsing from a system that does not have debugging access.
+By default, for security reasons, only a browser originating from
+``localhost`` (``127.0.0.1``) can see the debug toolbar. To allow your
+browser on a remote system to access the server, add the a line within the
+``[app:main]`` section of the ``development.ini`` file in the form
+``debugtoolbar.hosts = X.X.X.X``. For example, if your Pyramid application
+is running on a remote system, and you're browsing from a host with the IP
+address ``192.168.1.1``, you'd add something like this to enable the toolbar
+when your system contacts Pyramid:
+
+.. code-block:: ini
+
+ [app:main]
+ # .. other settings ...
+ debugtoolbar.hosts = 192.168.1.1
+
For more information about what the debug toolbar allows you to do, see `the
documentation for pyramid_debugtoolbar
-<http://docs.pylonsproject.org/projects/pyramid_debugtoolbar/dev/>`_.
+<http://docs.pylonsproject.org/projects/pyramid_debugtoolbar/en/latest/>`_.
The debug toolbar will not be shown (and all debugging will be turned off)
when you use the ``production.ini`` file instead of the ``development.ini``
diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst
index ed391f4fe..1f1b1943b 100644
--- a/docs/narr/renderers.rst
+++ b/docs/narr/renderers.rst
@@ -156,7 +156,6 @@ dictionary:
.. code-block:: python
:linenos:
- from pyramid.response import Response
from pyramid.view import view_config
@view_config(renderer='string')
@@ -193,7 +192,6 @@ render the returned dictionary to a JSON serialization:
.. code-block:: python
:linenos:
- from pyramid.response import Response
from pyramid.view import view_config
@view_config(renderer='json')
@@ -335,15 +333,15 @@ dictionary, an error will be raised.
Before passing keywords to the template, the keyword arguments derived from
the dictionary returned by the view are augmented. The callable object --
-whatever object was used to define the view -- will be automatically
-inserted into the set of keyword arguments passed to the template as the
-``view`` keyword. If the view callable was a class, the ``view`` keyword
-will be an instance of that class. Also inserted into the keywords passed to
-the template are ``renderer_name`` (the string used in the ``renderer``
-attribute of the directive), ``renderer_info`` (an object containing
-renderer-related information), ``context`` (the context resource of the view
-used to render the template), and ``request`` (the request passed to the view
-used to render the template).
+whatever object was used to define the view -- will be automatically inserted
+into the set of keyword arguments passed to the template as the ``view``
+keyword. If the view callable was a class, the ``view`` keyword will be an
+instance of that class. Also inserted into the keywords passed to the
+template are ``renderer_name`` (the string used in the ``renderer`` attribute
+of the directive), ``renderer_info`` (an object containing renderer-related
+information), ``context`` (the context resource of the view used to render
+the template), and ``request`` (the request passed to the view used to render
+the template). ``request`` is also available as ``req`` in Pyramid 1.3+.
Here's an example view configuration which uses a Chameleon ZPT renderer:
diff --git a/docs/narr/resources.rst b/docs/narr/resources.rst
index 256f69fc3..a24c44f29 100644
--- a/docs/narr/resources.rst
+++ b/docs/narr/resources.rst
@@ -303,13 +303,22 @@ The ``__resource_url__`` hook is passed two arguments: ``request`` and
two keys:
``physical_path``
- The "physical path" computed for the resource, as defined by
- ``pyramid.traversal.resource_path(resource)``.
+ A string representing the "physical path" computed for the resource, as
+ defined by ``pyramid.traversal.resource_path(resource)``. It will begin
+ and end with a slash.
``virtual_path``
- The "virtual path" computed for the resource, as defined by
- :ref:`virtual_root_support`. This will be identical to the physical path
- if virtual rooting is not enabled.
+ A string representing the "virtual path" computed for the resource, as
+ defined by :ref:`virtual_root_support`. This will be identical to the
+ physical path if virtual rooting is not enabled. It will begin and end
+ with a slash.
+
+``app_url``
+ A string representing the application URL generated during
+ ``request.resource_url``. It will not end with a slash. 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 preferred over use of ``request.application_url``.
The ``__resource_url__`` method of a resource should return a string
representing a URL. If it cannot override the default, it should return
@@ -322,16 +331,16 @@ Here's an example ``__resource_url__`` method.
class Resource(object):
def __resource_url__(self, request, info):
- return request.application_url + info['virtual_path']
+ return info['app_url'] + info['virtual_path']
The above example actually just generates and returns the default URL, which
-would have been what was returned anyway, but your code can perform arbitrary
-logic as necessary. For example, your code may wish to override the hostname
-or port number of the generated URL.
+would have been what was generated by the default ``resource_url`` machinery,
+but your code can perform arbitrary logic as necessary. For example, your
+code may wish to override the hostname or port number of the generated URL.
Note that the URL generated by ``__resource_url__`` should be fully
qualified, should end in a slash, and should not contain any query string or
-anchor elements (only path elements) to work best with
+anchor elements (only path elements) to work with
:meth:`~pyramid.request.Request.resource_url`.
.. index::
@@ -540,14 +549,14 @@ declares that the blog entry implements an :term:`interface`.
:linenos:
import datetime
- from zope.interface import implements
+ from zope.interface import implementer
from zope.interface import Interface
class IBlogEntry(Interface):
pass
+ @implementer(IBlogEntry)
class BlogEntry(object):
- implements(IBlogEntry)
def __init__(self, title, body, author):
self.title = title
self.body = body
@@ -556,15 +565,15 @@ declares that the blog entry implements an :term:`interface`.
This resource consists of two things: the class which defines the resource
constructor as the class ``BlogEntry``, and an :term:`interface` attached to
-the class via an ``implements`` statement at class scope using the
-``IBlogEntry`` interface as its sole argument.
+the class via an ``implementer`` class decorator using the ``IBlogEntry``
+interface as its sole argument.
The interface object used must be an instance of a class that inherits from
:class:`zope.interface.Interface`.
A resource class may implement zero or more interfaces. You specify that a
resource implements an interface by using the
-:func:`zope.interface.implements` function at class scope. The above
+:func:`zope.interface.implementer` function as a class decorator. The above
``BlogEntry`` resource implements the ``IBlogEntry`` interface.
You can also specify that a particular resource *instance* provides an
diff --git a/docs/narr/traversal.rst b/docs/narr/traversal.rst
index 8c5d950c1..8e7f93a1b 100644
--- a/docs/narr/traversal.rst
+++ b/docs/narr/traversal.rst
@@ -488,20 +488,21 @@ you must create an interface and mark up your resource classes or instances
with interface declarations that refer to this interface.
To attach an interface to a resource *class*, you define the interface and
-use the :func:`zope.interface.implements` function to associate the interface
-with the class.
+use the :func:`zope.interface.implementer` class decorator to associate the
+interface with the class.
.. code-block:: python
:linenos:
from zope.interface import Interface
- from zope.interface import implements
+ from zope.interface import implementer
class IHello(Interface):
""" A marker interface """
+ @implementer(IHello)
class Hello(object):
- implements(IHello)
+ pass
To attach an interface to a resource *instance*, you define the interface and
use the :func:`zope.interface.alsoProvides` function to associate the
diff --git a/docs/tutorials/.gitignore b/docs/tutorials/.gitignore
new file mode 100644
index 000000000..71e689620
--- /dev/null
+++ b/docs/tutorials/.gitignore
@@ -0,0 +1 @@
+env*/
diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst
index 56237a1b9..b1d0bf37c 100644
--- a/docs/tutorials/wiki2/authorization.rst
+++ b/docs/tutorials/wiki2/authorization.rst
@@ -159,14 +159,14 @@ logged in user and redirect back to the front page.
The ``login`` view callable will look something like this:
.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 90-116
+ :lines: 87-113
:linenos:
:language: python
The ``logout`` view callable will look something like this:
.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 118-122
+ :lines: 115-119
:linenos:
:language: python
diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst
index bda0a2eb7..a067dbd66 100644
--- a/docs/tutorials/wiki2/definingviews.rst
+++ b/docs/tutorials/wiki2/definingviews.rst
@@ -126,7 +126,7 @@ HTML anchor for each *WikiWord* reference in the rendered HTML using a
compiled regular expression.
.. literalinclude:: src/views/tutorial/views.py
- :lines: 23-44
+ :lines: 23-43
:linenos:
:language: python
@@ -161,7 +161,7 @@ The ``matchdict`` attribute of the request passed to the ``add_page`` view
will have the values we need to construct URLs and find model objects.
.. literalinclude:: src/views/tutorial/views.py
- :lines: 46-58
+ :lines: 45-56
:linenos:
:language: python
@@ -184,7 +184,7 @@ If the view execution *is* a result of a form submission (if the expression
``'form.submitted' in request.params`` is ``True``), we scrape the page body
from the form data, create a Page object with this page body and the name
taken from ``matchdict['pagename']``, and save it into the database using
-``session.add``. We then redirect back to the ``view_page`` view for the
+``DBSession.add``. We then redirect back to the ``view_page`` view for the
newly created page.
The ``edit_page`` view function
@@ -197,7 +197,7 @@ request passed to the ``edit_page`` view will have a ``'pagename'`` key
matching the name of the page the user wants to edit.
.. literalinclude:: src/views/tutorial/views.py
- :lines: 60-73
+ :lines: 58-70
:linenos:
:language: python
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views.py b/docs/tutorials/wiki2/src/authorization/tutorial/views.py
index 375f1f5a5..087e6076b 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/views.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/views.py
@@ -33,14 +33,13 @@ def view_wiki(request):
@view_config(route_name='view_page', renderer='templates/view.pt')
def view_page(request):
pagename = request.matchdict['pagename']
- session = DBSession()
- page = session.query(Page).filter_by(name=pagename).first()
+ page = DBSession.query(Page).filter_by(name=pagename).first()
if page is None:
return HTTPNotFound('No such page')
def check(match):
word = match.group(1)
- exists = session.query(Page).filter_by(name=word).all()
+ exists = DBSession.query(Page).filter_by(name=word).all()
if exists:
view_url = request.route_url('view_page', pagename=word)
return '<a href="%s">%s</a>' % (view_url, word)
@@ -59,10 +58,9 @@ def view_page(request):
def add_page(request):
name = request.matchdict['pagename']
if 'form.submitted' in request.params:
- session = DBSession()
body = request.params['body']
page = Page(name, body)
- session.add(page)
+ DBSession.add(page)
return HTTPFound(location = request.route_url('view_page',
pagename=name))
save_url = request.route_url('add_page', pagename=name)
@@ -74,11 +72,10 @@ def add_page(request):
permission='edit')
def edit_page(request):
name = request.matchdict['pagename']
- session = DBSession()
- page = session.query(Page).filter_by(name=name).one()
+ page = DBSession.query(Page).filter_by(name=name).one()
if 'form.submitted' in request.params:
page.data = request.params['body']
- session.add(page)
+ DBSession.add(page)
return HTTPFound(location = request.route_url('view_page',
pagename=name))
return dict(
diff --git a/docs/tutorials/wiki2/src/views/tutorial/views.py b/docs/tutorials/wiki2/src/views/tutorial/views.py
index 5c49dd2e8..c2a94a96b 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/views.py
+++ b/docs/tutorials/wiki2/src/views/tutorial/views.py
@@ -23,14 +23,13 @@ def view_wiki(request):
@view_config(route_name='view_page', renderer='templates/view.pt')
def view_page(request):
pagename = request.matchdict['pagename']
- session = DBSession()
- page = session.query(Page).filter_by(name=pagename).first()
+ page = DBSession.query(Page).filter_by(name=pagename).first()
if page is None:
return HTTPNotFound('No such page')
def check(match):
word = match.group(1)
- exists = session.query(Page).filter_by(name=word).all()
+ exists = DBSession.query(Page).filter_by(name=word).all()
if exists:
view_url = request.route_url('view_page', pagename=word)
return '<a href="%s">%s</a>' % (view_url, word)
@@ -47,10 +46,9 @@ def view_page(request):
def add_page(request):
name = request.matchdict['pagename']
if 'form.submitted' in request.params:
- session = DBSession()
body = request.params['body']
page = Page(name, body)
- session.add(page)
+ DBSession.add(page)
return HTTPFound(location = request.route_url('view_page',
pagename=name))
save_url = request.route_url('add_page', pagename=name)
@@ -60,11 +58,10 @@ def add_page(request):
@view_config(route_name='edit_page', renderer='templates/edit.pt')
def edit_page(request):
name = request.matchdict['pagename']
- session = DBSession()
- page = session.query(Page).filter_by(name=name).one()
+ page = DBSession.query(Page).filter_by(name=name).one()
if 'form.submitted' in request.params:
page.data = request.params['body']
- session.add(page)
+ DBSession.add(page)
return HTTPFound(location = request.route_url('view_page',
pagename=name))
return dict(
diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst
index b6cfde039..acb884d49 100644
--- a/docs/whatsnew-1.3.rst
+++ b/docs/whatsnew-1.3.rst
@@ -54,7 +54,8 @@ to make some changes:
- We've replaced the ``paster`` command with Pyramid-specific analogues.
-- We've made the default WSGI server the ``waitress`` server.
+- We've made the default WSGI server used by Pyramid scaffolding the
+ :term:`waitress` server.
Previously (in Pyramid 1.0, 1.1 and 1.2), you created a Pyramid application
using ``paster create``, like so::
@@ -259,6 +260,74 @@ 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.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
+ more information about how to use the ``ignore`` argument to ``scan``.
+
+- Better error messages when a view callable returns a value that cannot be
+ converted to a response (for example, when a view callable returns a
+ dictionary without a renderer defined, or doesn't return any value at all).
+ The error message now contains information about the view callable itself
+ as well as the result of calling it.
+
+- Better error message when a .pyc-only module is ``config.include`` -ed.
+ This is not permitted due to error reporting requirements, and a better
+ error message is shown when it is attempted. Previously it would fail with
+ something like "AttributeError: 'NoneType' object has no attribute
+ 'rfind'".
+
+- 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
+ purely a change to reduce the amount of typing required to use request
+ 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
---------------------------
@@ -295,9 +364,10 @@ Backwards Incompatibilities
and upgrade Pyramid itself "in-place"; it may simply break instead
(particularly if you use ZCML's ``includeOverrides`` directive).
-- String values passed to ``route_url`` or ``route_path`` that are meant to
- replace "remainder" matches will now be URL-quoted except for embedded
- slashes. For example::
+- String values passed to :meth:`Pyramid.request.Request.route_url` or
+ :meth:`Pyramid.request.Request.route_path` that are meant to replace
+ "remainder" matches will now be URL-quoted except for embedded slashes. For
+ example::
config.add_route('remain', '/foo*remainder')
request.route_path('remain', remainder='abc / def')
@@ -316,8 +386,8 @@ Backwards Incompatibilities
``route_path`` or ``route_url`` to do this now.
- If you pass a bytestring that contains non-ASCII characters to
- ``add_route`` as a pattern, it will now fail at startup time. Use Unicode
- instead.
+ :meth:`pyramid.config.Configurator.add_route` as a pattern, it will now
+ fail at startup time. Use Unicode instead.
- The ``path_info`` route and view predicates now match against
``request.upath_info`` (Unicode) rather than ``request.path_info``
@@ -328,6 +398,22 @@ 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 an adapter using it as
+ documented in older versions 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
--------------------------
@@ -375,6 +461,8 @@ Dependency Changes
- Pyramid no longer depends on the ``Paste`` or ``PasteScript`` packages.
These packages are not Python 3 compatible.
+- Depend on ``venusian`` >= 1.0a3 to provide scan ``ignore`` support.
+
Scaffolding Changes
-------------------
diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py
index 1656b5410..06d3c6abf 100644
--- a/pyramid/config/__init__.py
+++ b/pyramid/config/__init__.py
@@ -253,6 +253,7 @@ class Configurator(
info = ''
object_description = staticmethod(object_description)
introspectable = Introspectable
+ inspect = inspect
def __init__(self,
registry=None,
@@ -706,7 +707,7 @@ class Configurator(
route_prefix = None
c = self.maybe_dotted(callable)
- module = inspect.getmodule(c)
+ module = self.inspect.getmodule(c)
if module is c:
try:
c = getattr(module, 'includeme')
@@ -716,7 +717,13 @@ class Configurator(
)
spec = module.__name__ + ':' + c.__name__
- sourcefile = inspect.getsourcefile(c)
+ sourcefile = self.inspect.getsourcefile(c)
+
+ if sourcefile is None:
+ raise ConfigurationError(
+ 'No source file for module %r (.py file must exist, '
+ 'refusing to use orphan .pyc or .pyo file).' % module.__name__)
+
if action_state.processSpec(spec):
configurator = self.__class__(
diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py
index eb4442e98..76f8d86ed 100644
--- a/pyramid/config/factories.py
+++ b/pyramid/config/factories.py
@@ -1,3 +1,5 @@
+from zope.interface import Interface
+
from pyramid.config.util import action_method
from pyramid.interfaces import (
@@ -7,6 +9,8 @@ from pyramid.interfaces import (
IRequestProperties,
IRootFactory,
ISessionFactory,
+ ITraverser,
+ IResourceURL,
)
from pyramid.traversal import DefaultRootFactory
@@ -140,6 +144,145 @@ class FactoriesConfiguratorMixin(object):
self.action(('request properties', name), register,
introspectables=(intr,))
+ @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
+ necessary, this default algorithm can be swapped out selectively for
+ a different traversal pattern via configuration. The section
+ entitled :ref:`changing_the_traverser` details how to create a
+ traverser class.
+
+ For example, to override the superdefault traverser used by Pyramid,
+ you might do something like this:
+
+ .. code-block:: python
+
+ from myapp.traversal import 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``
+ class, no matter which object was returned by the :term:`root
+ factory` of this application. Note that we passed no arguments to
+ the ``iface`` keyword parameter. The default value of ``iface``,
+ ``None`` represents that the registered traverser should be used when
+ no other more specific traverser is available for the object returned
+ by the root factory.
+
+ However, more than one traversal algorithm can be active at the same
+ time. The traverser used can depend on the result of the :term:`root
+ factory`. For instance, if your root factory returns more than one
+ type of object conditionally, you could claim that an alternate
+ traverser adapter should be used agsinst one particular class or
+ interface returned by that root factory. When the root factory
+ returned an object that implemented that class or interface, a custom
+ traverser would be used. Otherwise, the default traverser would be
+ used. The ``iface`` argument represents the class of the object that
+ the root factory might return or an :term:`interface` that the object
+ might implement.
+
+ To use a particular traverser only when the root factory returns a
+ particular class:
+
+ .. code-block:: python
+
+ 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
+ value returned by the root factory most closely).
+
+ Note that either ``factory`` or ``iface`` can be a :term:`dotted
+ Python name` or a Python object.
+
+ See :ref:`changing_the_traverser` for more information.
+ """
+ iface = self.maybe_dotted(iface)
+ factory = self.maybe_dotted(factory)
+ def register(iface=iface):
+ if iface is None:
+ iface = Interface
+ self.registry.registerAdapter(factory, (iface,), ITraverser)
+ discriminator = ('traverser', iface)
+ intr = self.introspectable(
+ 'traversers',
+ discriminator,
+ 'traverser for %r' % iface,
+ 'traverser',
+ )
+ intr['factory'] = factory
+ intr['iface'] = iface
+ 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
plist = request.registry.queryUtility(IRequestProperties)
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index 1988b532b..9d2e15537 100644
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -72,6 +72,7 @@ def view_description(view):
try:
return view.__text__
except AttributeError:
+ # custom view mappers might not add __text__
return object_description(view)
def wraps_view(wrapper):
@@ -407,11 +408,15 @@ class DefaultViewMapper(object):
mapped_view = self.map_nonclass_requestonly(view)
elif self.attr:
mapped_view = self.map_nonclass_attr(view)
- if self.attr is not None:
- mapped_view.__text__ = 'attr %s of %s' % (
- self.attr, object_description(view))
- else:
- mapped_view.__text__ = object_description(view)
+ if inspect.isroutine(mapped_view):
+ # we potentially mutate an unwrapped view here if it's a function;
+ # we do this to avoid function call overhead of injecting another
+ # wrapper
+ if self.attr is not None:
+ mapped_view.__text__ = 'attr %s of %s' % (
+ self.attr, object_description(view))
+ else:
+ mapped_view.__text__ = object_description(view)
return mapped_view
def map_class_requestonly(self, view):
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/renderers.py b/pyramid/renderers.py
index 61f5e0b35..14941c61a 100644
--- a/pyramid/renderers.py
+++ b/pyramid/renderers.py
@@ -390,7 +390,8 @@ class RendererHelper(object):
'renderer_name':self.name, # b/c
'renderer_info':self,
'context':context,
- 'request':request
+ 'request':request,
+ 'req':request,
}
return self.render_to_response(response, system, request=request)
@@ -403,6 +404,7 @@ class RendererHelper(object):
'renderer_info':self,
'context':getattr(request, 'context', None),
'request':request,
+ 'req':request,
}
system_values = BeforeRender(system_values, value)
diff --git a/pyramid/scaffolds/__init__.py b/pyramid/scaffolds/__init__.py
index ab2b3034a..ad5753713 100644
--- a/pyramid/scaffolds/__init__.py
+++ b/pyramid/scaffolds/__init__.py
@@ -1,5 +1,6 @@
import binascii
import os
+import sys
from pyramid.compat import native_
@@ -52,10 +53,14 @@ class AlchemyProjectTemplate(PyramidTemplate):
summary = 'Pyramid SQLAlchemy project using url dispatch'
def post(self, command, output_dir, vars): # pragma: no cover
val = PyramidTemplate.post(self, command, output_dir, vars)
+ vars = vars.copy()
+ vars['output_dir'] = output_dir
+ vars['pybin'] = os.path.join(sys.exec_prefix, 'bin')
self.out('')
self.out('Please run the "populate_%(project)s" script to set up the '
- 'SQL database after installing (but before starting) the '
- 'application (e.g. '
- '"$myvirtualenv/bin/populate_%(project)s development.ini".)'
+ 'SQL database after\ninstalling (but before starting) the '
+ 'application.\n\n For example:\n\ncd %(output_dir)s\n'
+ '%(pybin)s/python setup.py develop\n'
+ '%(pybin)s/populate_%(project)s development.ini'
% vars)
return val
diff --git a/pyramid/tests/test_config/test_factories.py b/pyramid/tests/test_config/test_factories.py
index d1a01568f..5f300a73e 100644
--- a/pyramid/tests/test_config/test_factories.py
+++ b/pyramid/tests/test_config/test_factories.py
@@ -129,6 +129,110 @@ class TestFactoriesMixin(unittest.TestCase):
self.assertEqual(callables, [('foo', foo, False),
('bar', foo, True)])
+ def test_add_traverser_dotted_names(self):
+ from pyramid.interfaces import ITraverser
+ config = self._makeOne(autocommit=True)
+ config.add_traverser(
+ 'pyramid.tests.test_config.test_factories.DummyTraverser',
+ 'pyramid.tests.test_config.test_factories.DummyIface')
+ iface = DummyIface()
+ traverser = config.registry.getAdapter(iface, ITraverser)
+ self.assertEqual(traverser.__class__, DummyTraverser)
+ self.assertEqual(traverser.root, iface)
+
+ def test_add_traverser_default_iface_means_Interface(self):
+ from pyramid.interfaces import ITraverser
+ config = self._makeOne(autocommit=True)
+ config.add_traverser(DummyTraverser)
+ traverser = config.registry.getAdapter(None, ITraverser)
+ self.assertEqual(traverser.__class__, DummyTraverser)
+
+ def test_add_traverser_nondefault_iface(self):
+ from pyramid.interfaces import ITraverser
+ config = self._makeOne(autocommit=True)
+ 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_add_traverser_introspectables(self):
+ config = self._makeOne()
+ config.add_traverser(DummyTraverser, 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, 'traverser')
+ self.assertEqual(intr.discriminator, ('traverser', DummyIface))
+ 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
@@ -139,3 +243,16 @@ class DummyRequest(object):
if self.callables is None:
self.callables = []
self.callables.append((name, callable, reify))
+
+class DummyTraverser(object):
+ def __init__(self, root):
+ self.root = root
+
+class DummyIface(object):
+ pass
+
+class DummyResourceURL(object):
+ def __init__(self, resource, request):
+ self.resource = resource
+ self.request = request
+
diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py
index d237b3fe8..283800e1e 100644
--- a/pyramid/tests/test_config/test_init.py
+++ b/pyramid/tests/test_config/test_init.py
@@ -739,6 +739,26 @@ pyramid.tests.test_config.dummy_include2""",
root_config.include(dummy_subapp, route_prefix='nested')
+ def test_include_with_missing_source_file(self):
+ from pyramid.exceptions import ConfigurationError
+ import inspect
+ config = self._makeOne()
+ class DummyInspect(object):
+ def getmodule(self, c):
+ return inspect.getmodule(c)
+ def getsourcefile(self, c):
+ return None
+ config.inspect = DummyInspect()
+ try:
+ config.include('pyramid.tests.test_config.dummy_include')
+ except ConfigurationError as e:
+ self.assertEqual(
+ e.args[0],
+ "No source file for module 'pyramid.tests.test_config' (.py "
+ "file must exist, refusing to use orphan .pyc or .pyo file).")
+ else: # pragma: no cover
+ raise AssertionError
+
def test_with_context(self):
config = self._makeOne()
context = DummyZCMLContext()
diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py
index dbdfb06b3..b32e68e25 100644
--- a/pyramid/tests/test_renderers.py
+++ b/pyramid/tests/test_renderers.py
@@ -484,7 +484,8 @@ class TestRendererHelper(unittest.TestCase):
'renderer_name': 'loo.foo',
'request': request,
'context': 'context',
- 'view': 'view'}
+ 'view': 'view',
+ 'req': request,}
)
def test_render_explicit_registry(self):
@@ -517,7 +518,8 @@ class TestRendererHelper(unittest.TestCase):
'context':context,
'renderer_name':'loo.foo',
'view':None,
- 'renderer_info':helper
+ 'renderer_info':helper,
+ 'req':request,
}
self.assertEqual(result[0], 'values')
self.assertEqual(result[1], system)
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..efcf241b7 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,20 @@ class URLMethodsMixin(object):
element 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
+ ``_host='foo.com'``, and the URL that would have been generated
+ without the host replacement is ``http://example.com/a``, the result
+ will be ``https://foo.com/a``.
+
+ Note that 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 +185,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 +213,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 +228,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 +252,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 +291,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 +311,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 +364,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
+ ``host='foo.com'``, and the URL that would have been generated
+ without the host replacement is ``http://example.com/a``, the result
+ will be ``https://foo.com/a``.
+
+ 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 +426,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 +511,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`.