diff options
| author | Chris McDonough <chrism@plope.com> | 2012-02-17 18:29:44 -0500 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2012-02-17 18:29:44 -0500 |
| commit | 222638ad3760692ac0ec5368db4baaee9272e818 (patch) | |
| tree | b8fc997119a2b470db290c8270241e8991eb797e | |
| parent | cdb5f5a395c53e18a250651b6c9c3e0322b0dfe5 (diff) | |
| parent | 305d23f9e9dd095f4fdface116a2155bd86a453c (diff) | |
| download | pyramid-222638ad3760692ac0ec5368db4baaee9272e818.tar.gz pyramid-222638ad3760692ac0ec5368db4baaee9272e818.tar.bz2 pyramid-222638ad3760692ac0ec5368db4baaee9272e818.zip | |
Merge branch '1.3-branch'
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 ------------ @@ -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`. |
