summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt42
-rw-r--r--TODO.txt3
-rw-r--r--docs/conf.py2
-rw-r--r--docs/narr/advconfig.rst12
-rw-r--r--docs/narr/assets.rst7
-rw-r--r--docs/narr/environment.rst63
-rw-r--r--docs/narr/hybrid.rst59
-rw-r--r--docs/narr/urldispatch.rst152
-rw-r--r--docs/narr/viewconfig.rst3
-rw-r--r--docs/tutorials/wiki2/authorization.rst12
-rw-r--r--docs/tutorials/wiki2/basiclayout.rst30
-rw-r--r--docs/tutorials/wiki2/definingviews.rst38
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/__init__.py39
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py5
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/__init__.py5
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/__init__.py21
-rw-r--r--pyramid/compat/__init__.py9
-rw-r--r--pyramid/config.py131
-rw-r--r--pyramid/paster_templates/routesalchemy/+package+/__init__.py_tmpl7
-rw-r--r--pyramid/request.py52
-rw-r--r--pyramid/static.py60
-rw-r--r--pyramid/tests/ccbugapp/__init__.py11
-rw-r--r--pyramid/tests/exceptionviewapp/__init__.py26
-rw-r--r--pyramid/tests/hybridapp/__init__.py3
-rw-r--r--pyramid/tests/restbugapp/__init__.py23
-rw-r--r--pyramid/tests/test_config.py364
-rw-r--r--pyramid/tests/test_integration.py20
-rw-r--r--pyramid/tests/test_request.py67
-rw-r--r--pyramid/tests/test_static.py93
-rw-r--r--pyramid/tests/test_wsgi.py48
-rw-r--r--pyramid/tests/wsgiapp2app/__init__.py17
-rw-r--r--pyramid/wsgi.py40
-rw-r--r--setup.py2
33 files changed, 940 insertions, 526 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 5f08606be..d329c260d 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -25,6 +25,10 @@ Documentation
``pyramid.config.Configurator.set_view_mapper`` and refer to it within
Hooks chapter section named "Using a View Mapper".
+- Added section to the "Environment Variables and ``.ini`` File Settings"
+ chapter in the narrative documentation section entitled "Adding a Custom
+ Setting".
+
Features
--------
@@ -136,6 +140,13 @@ Bug Fixes
- Redirects issued by a static view did not take into account any existing
``SCRIPT_NAME`` (such as one set by a url mapping composite). Now they do.
+- The ``pyramid.wsgi.wsgiapp2`` decorator did not take into account the
+ ``SCRIPT_NAME`` in the origin request.
+
+- The ``pyramid.wsgi.wsgiapp2`` decorator effectively only worked when it
+ decorated a view found via traversal; it ignored the ``PATH_INFO`` that was
+ part of a url-dispatch-matched view.
+
Deprecations
------------
@@ -151,6 +162,31 @@ Deprecations
``request.response_content_type = 'abc'`` should be changed to
``request.response.content_type = 'abc'``).
+- Passing view-related parameters to
+ ``pyramid.config.Configurator.add_route`` is now deprecated. Previously, a
+ view was permitted to be connected to a route using a set of ``view*``
+ parameters passed to the ``add_route`` method of the Configurator. This
+ was a shorthand which replaced the need to perform a subsequent call to
+ ``add_view``. For example, it was valid (and often recommended) to do::
+
+ config.add_route('home', '/', view='mypackage.views.myview',
+ view_renderer='some/renderer.pt')
+
+ Passing ``view*`` arguments to ``add_route`` is now deprecated in favor of
+ connecting a view to a predefined route via ``Configurator.add_view`` using
+ the route's ``route_name`` parameter. As a result, the above example
+ should now be spelled::
+
+ config.add_route('home', '/')
+ config.add_view('mypackage.views.myview', route_name='home')
+ renderer='some/renderer.pt')
+
+ This deprecation was done to reduce confusion observed in IRC, as well as
+ to (eventually) reduce documentation burden (see also
+ https://github.com/Pylons/pyramid/issues/164). A deprecation warning is
+ now issued when any view-related parameter is passed to
+ ``Configurator.add_route``.
+
Behavior Changes
----------------
@@ -169,6 +205,12 @@ Behavior Changes
renderer changes the content type (to ``application/json`` or
``text/plain`` for JSON and string renderers respectively).
+- The ``pyramid.wsgi.wsgiapp2`` now uses a slightly different method of
+ figuring out how to "fix" ``SCRIPT_NAME`` and ``PATH_INFO`` for the
+ downstream application. As a result, those values may differ slightly from
+ the perspective of the downstream application (for example, ``SCRIPT_NAME``
+ will now never possess a trailing slash).
+
Dependencies
------------
diff --git a/TODO.txt b/TODO.txt
index eccfb34ed..ac915955c 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -89,9 +89,6 @@ Nice-to-Have
action = '^foo$'
mypackage.views.MyView.foo_GET
-- Raise an exception when a value in response_headerlist is not a
- string or decide to encode.
-
- Update App engine chapter with less creaky directions.
- Add functionality that mocks the behavior of ``repoze.browserid``.
diff --git a/docs/conf.py b/docs/conf.py
index a987106d4..a610351ff 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -93,7 +93,7 @@ copyright = '%s, Agendaless Consulting' % datetime.datetime.now().year
# other places throughout the built documents.
#
# The short X.Y version.
-version = '1.0'
+version = '1.1a0'
# The full version, including alpha/beta/rc tags.
release = version
diff --git a/docs/narr/advconfig.rst b/docs/narr/advconfig.rst
index 099bce35f..5ee554284 100644
--- a/docs/narr/advconfig.rst
+++ b/docs/narr/advconfig.rst
@@ -295,15 +295,9 @@ These are the methods of the configurator which provide conflict detection:
:meth:`~pyramid.config.Configurator.set_locale_negotiator` and
:meth:`~pyramid.config.Configurator.set_default_permission`.
-Some other methods of the configurator also indirectly provide conflict
-detection, because they're implemented in terms of conflict-aware methods:
-
-- :meth:`~pyramid.config.Configurator.add_route` does a second type of
- conflict detection when a ``view`` parameter is passed (it calls
- ``add_view``).
-
-- :meth:`~pyramid.config.Configurator.static_view`, a frontend for
- ``add_route`` and ``add_view``.
+:meth:`~pyramid.config.Configurator.add_static_view` also indirectly
+provides conflict detection, because it's implemented in terms of the
+conflict-aware ``add_route`` and ``add_view`` methods.
.. _including_configuration:
diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst
index bbb673ecc..8d0e7058c 100644
--- a/docs/narr/assets.rst
+++ b/docs/narr/assets.rst
@@ -341,7 +341,8 @@ application's startup code.
# .. every other add_route declaration should come
# before this one, as it will, by default, catch all requests
- config.add_route('catchall_static', '/*subpath', 'myapp.static.static_view')
+ config.add_route('catchall_static', '/*subpath')
+ config.add_view('myapp.static.static_view', route_name='catchall_static')
The special name ``*subpath`` above is used by the
:class:`~pyramid.view.static` view callable to signify the path of the file
@@ -384,8 +385,8 @@ Or you might register it to be the view callable for a particular route:
.. code-block:: python
:linenos:
- config.add_route('favicon', '/favicon.ico',
- view='myapp.views.favicon_view')
+ config.add_route('favicon', '/favicon.ico')
+ config.add_view('myapp.views.favicon_view', route_name='favicon')
Because this is a simple view callable, it can be protected with a
:term:`permission` or can be configured to respond under different
diff --git a/docs/narr/environment.rst b/docs/narr/environment.rst
index 7b7946aae..8c299f3a3 100644
--- a/docs/narr/environment.rst
+++ b/docs/narr/environment.rst
@@ -381,3 +381,66 @@ around in overridden asset directories. ``reload_assets`` makes the system
*very slow* when templates are in use. Never set ``reload_assets`` to
``True`` on a production system.
+Adding A Custom Setting
+-----------------------
+
+From time to time, you may need to add a custom setting to your application.
+Here's how:
+
+- If you're using an ``.ini`` file, change the ``.ini`` file, adding the
+ setting to the ``[app:foo]`` section representing your Pyramid application.
+ For example:
+
+ .. code-block:: ini
+
+ [app:myapp]
+ # .. other settings
+ debug_frobnosticator = True
+
+- In the ``main()`` function that represents the place that your Pyramid WSGI
+ application is created, anticipate that you'll be getting this key/value
+ pair as a setting and do any type conversion necessary.
+
+ If you've done any type conversion of your custom value, reset the
+ converted values into the ``settings`` dictionary *before* you pass the
+ dictionary as ``settings`` to the :term:`Configurator`. For example:
+
+ .. code-block:: python
+
+ def main(global_config, **settings):
+ # ...
+ from pyramid.settings import asbool
+ debug_frobnosticator = asbool(settings.get(
+ 'debug_frobnosticator', 'false'))
+ settings['debug_frobnosticator'] = debug_frobnosticator
+ config = Configurator(settings=settings)
+
+ .. note:: It's especially important that you mutate the ``settings``
+ dictionary with the converted version of the variable *before* passing
+ it to the Configurator: the configurator makes a *copy* of ``settings``,
+ it doesn't use the one you pass directly.
+
+- In the runtime code that you need to access the new settings value, find
+ the value in the ``registry.settings`` dictionary and use it. In
+ :term:`view` code (or any other code that has access to the request), the
+ easiest way to do this is via ``request.registry.settings``. For example:
+
+ .. code-block:: python
+
+ registry = request.registry.settings
+ debug_frobnosticator = settings['debug_frobnosticator']
+
+ If you wish to use the value in code that does not have access to the
+ request and you wish to use the value, you'll need to use the
+ :func:`pyramid.threadlocal.get_current_registry` API to obtain the current
+ registry, then ask for its ``settings`` attribute. For example:
+
+ .. code-block:: python
+
+ registry = pyramid.threadlocal.get_current_registry()
+ settings = registry.settings
+ debug_frobnosticator = settings['debug_frobnosticator']
+
+
+
+
diff --git a/docs/narr/hybrid.rst b/docs/narr/hybrid.rst
index 780cb0975..f8ed743fb 100644
--- a/docs/narr/hybrid.rst
+++ b/docs/narr/hybrid.rst
@@ -33,7 +33,7 @@ URL Dispatch Only
~~~~~~~~~~~~~~~~~
An application that uses :term:`url dispatch` exclusively to map URLs to code
-will often have statements like this within your application startup
+will often have statements like this within application startup
configuration:
.. code-block:: python
@@ -41,15 +41,20 @@ configuration:
# config is an instance of pyramid.config.Configurator
- config.add_route('foobar', '{foo}/{bar}', view='myproject.views.foobar')
- config.add_route('bazbuz', '{baz}/{buz}', view='myproject.views.bazbuz')
+ config.add_route('foobar', '{foo}/{bar}')
+ config.add_route('bazbuz', '{baz}/{buz}')
-Each :term:`route` typically corresponds to a single view callable,
-and when that route is matched during a request, the view callable
-named by the ``view`` attribute is invoked.
+ config.add_view('myproject.views.foobar', route_name='foobar')
+ config.add_view('myproject.views.bazbuz', route_name='bazbuz')
-Typically, an application that uses only URL dispatch won't perform any calls
-to :meth:`pyramid.config.Configurator.add_view` in its startup code.
+Each :term:`route` corresponds to one or more view callables. Each view
+callable is associated with a route by passing a ``route_name`` parameter
+that matches its name during a call to
+:meth:`~pyramid.config.Configurator.add_view`. When a route is matched
+during a request, :term:`view lookup` is used to match the request to its
+associated view callable. The presence of calls to
+:meth:`~pyramid.config.Configurator.add_route` signify that an application is
+using URL dispatch.
Traversal Only
~~~~~~~~~~~~~~
@@ -196,12 +201,9 @@ remainder becomes the path used to perform traversal.
The ``*remainder`` route pattern syntax is explained in more
detail within :ref:`route_pattern_syntax`.
-Note that unlike the examples provided within :ref:`urldispatch_chapter`, the
-``add_route`` configuration statement named previously does not pass a
-``view`` argument. This is because a hybrid mode application relies on
-:term:`traversal` to do :term:`resource location` and :term:`view lookup`
-instead of invariably invoking a specific view callable named directly within
-the matched route's configuration.
+A hybrid mode application relies more heavily on :term:`traversal` to do
+:term:`resource location` and :term:`view lookup` than most examples indicate
+within :ref:`urldispatch_chapter`.
Because the pattern of the above route ends with ``*traverse``, when this
route configuration is matched during a request, :app:`Pyramid` will attempt
@@ -426,13 +428,11 @@ attribute.
Using ``*subpath`` in a Route Pattern
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-There are certain extremely rare cases when you'd like to influence
-the traversal :term:`subpath` when a route matches without actually
-performing traversal. For instance, the
-:func:`pyramid.wsgi.wsgiapp2` decorator and the
-:class:`pyramid.view.static` helper attempt to compute
-``PATH_INFO`` from the request's subpath, so it's useful to be able to
-influence this value.
+There are certain extremely rare cases when you'd like to influence the
+traversal :term:`subpath` when a route matches without actually performing
+traversal. For instance, the :func:`pyramid.wsgi.wsgiapp2` decorator and the
+:class:`pyramid.view.static` helper attempt to compute ``PATH_INFO`` from the
+request's subpath, so it's useful to be able to influence this value.
When ``*subpath`` exists in a pattern, no path is actually traversed,
but the traversal algorithm will return a :term:`subpath` list implied
@@ -442,8 +442,8 @@ commonly in route declarations that look like this:
.. code-block:: python
:linenos:
- config.add_route('static', '/static/*subpath',
- view='mypackage.views.static_view')
+ config.add_route('static', '/static/*subpath')
+ config.add_view('mypackage.views.static_view', route_name='static')
Where ``mypackage.views.static_view`` is an instance of
:class:`pyramid.view.static`. This effectively tells the static helper to
@@ -458,11 +458,16 @@ application. We'll detail them here.
Registering a Default View for a Route That Has a ``view`` Attribute
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.. warning:: As of :app:`Pyramid` 1.1 this section is slated to be removed in
+ a later documentation release because the the ability to add views
+ directly to the :term:`route configuration` by passing a ``view`` argument
+ to ``add_route`` has been deprecated.
+
It is an error to provide *both* a ``view`` argument to a :term:`route
configuration` *and* a :term:`view configuration` which names a
``route_name`` that has no ``name`` value or the empty ``name`` value. For
-example, this pair of declarations will generate a "conflict" error at
-startup time.
+example, this pair of declarations will generate a conflict error at startup
+time.
.. code-block:: python
:linenos:
@@ -490,8 +495,8 @@ Can also be spelled like so:
config.add_route('home', '{foo}/{bar}/*traverse')
config.add_view('myproject.views.home', route_name='home')
-The two spellings are logically equivalent. In fact, the former is
-just a syntactical shortcut for the latter.
+The two spellings are logically equivalent. In fact, the former is just a
+syntactical shortcut for the latter.
Binding Extra Views Against a Route Configuration that Doesn't Have a ``*traverse`` Element In Its Pattern
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst
index 219753882..4923fd19f 100644
--- a/docs/narr/urldispatch.rst
+++ b/docs/narr/urldispatch.rst
@@ -54,17 +54,19 @@ Route Configuration
-------------------
:term:`Route configuration` is the act of adding a new :term:`route` to an
-application. A route has a *pattern*, representing a pattern meant to match
+application. A route has a *name*, which acts as an identifier to be used
+for URL generation. The name also allows developers to associate a view
+configuration with the route. A route also has a *pattern*, meant to match
against the ``PATH_INFO`` portion of a URL (the portion following the scheme
-and port, e.g. ``/foo/bar`` in the URL ``http://localhost:8080/foo/bar``),
-and a *route name*, which is used by developers within a :app:`Pyramid`
-application to uniquely identify a particular route when generating a URL.
-It also optionally has a ``factory``, a set of :term:`route predicate`
-parameters, and a set of :term:`view` parameters.
+and port, e.g. ``/foo/bar`` in the URL ``http://localhost:8080/foo/bar``). It
+also optionally has a ``factory`` and a set of :term:`route predicate`
+attributes.
.. index::
single: add_route
+.. _config-add-route:
+
Configuring a Route via The ``add_route`` Configurator Method
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -79,7 +81,8 @@ example:
# pyramid.config.Configurator class; "myview" is assumed
# to be a "view callable" function
from views import myview
- config.add_route('myroute', '/prefix/{one}/{two}', view=myview)
+ config.add_route('myroute', '/prefix/{one}/{two}')
+ config.add_view(myview, route_name='myroute')
.. versionchanged:: 1.0a4
Prior to 1.0a4, routes allow for a marker starting with a ``:``, for
@@ -89,9 +92,47 @@ example:
.. index::
single: route configuration; view callable
+.. _add_route_view_config:
+
Route Configuration That Names a View Callable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.. warning:: This section describes a feature which has been deprecated in
+ Pyramid 1.1 and higher. In order to reduce confusion and documentation
+ burden, passing view-related parameters to
+ :meth:`~pyramid.config.Configurator.add_route` is deprecated.
+
+ In versions earlier than 1.1, a view was permitted to be connected to a
+ route using a set of ``view*`` parameters passed to the
+ :meth:`~pyramid.config.Configurator.add_route`. This was a shorthand
+ which replaced the need to perform a subsequent call to
+ :meth:`~pyramid.config.Configurator.add_view` as described in
+ :ref:`config-add-route`. For example, it was valid (and often recommended)
+ to do:
+
+ .. code-block:: python
+
+ config.add_route('home', '/', view='mypackage.views.myview',
+ view_renderer='some/renderer.pt')
+
+ Instead of the equivalent:
+
+ .. code-block:: python
+
+ config.add_route('home', '/')
+ config.add_view('mypackage.views.myview', route_name='home')
+ renderer='some/renderer.pt')
+
+ Passing ``view*`` arguments to ``add_route`` as shown in the first
+ example above is now deprecated in favor of connecting a view to a
+ predefined route via :meth:`~pyramid.config.Configurator.add_view` using
+ the route's ``route_name`` parameter, as shown in the second example
+ above.
+
+ A deprecation warning is now issued when any view-related parameter is
+ passed to ``Configurator.add_route``. The recommended way to associate a
+ view with a route is documented in :ref:`config-add-route`.
+
When a route configuration declaration names a ``view`` attribute, the value
of the attribute will reference a :term:`view callable`. This view callable
will be invoked when the route matches. A view callable, as described in
@@ -125,6 +166,9 @@ When a route configuration names a ``view`` attribute, the :term:`view
callable` named as that ``view`` attribute will always be found and invoked
when the associated route pattern matches during a request.
+See :ref:`add_route_view_related_api` for a description of view-related
+arguments to ``add_route``.
+
.. index::
single: route path pattern syntax
@@ -363,8 +407,9 @@ resource of the view callable ultimately found via :term:`view lookup`.
.. code-block:: python
:linenos:
- config.add_route('abc', '/abc', view='myproject.views.theview',
+ config.add_route('abc', '/abc',
factory='myproject.resources.root_factory')
+ config.add_view('myproject.views.theview', route_name='abc')
The factory can either be a Python object or a :term:`dotted Python name` (a
string) which points to such a Python object, as it is above.
@@ -395,7 +440,8 @@ process. Examples of route predicate arguments are ``pattern``, ``xhr``, and
``request_method``.
Other arguments are view configuration related arguments. These only have an
-effect when the route configuration names a ``view``.
+effect when the route configuration names a ``view``. These arguments have
+been deprecated as of :app:`Pyramid` 1.1 (see :ref:`add_route_view_config`).
Other arguments are ``name`` and ``factory``. These arguments represent
neither predicates nor view configuration information.
@@ -547,8 +593,8 @@ If any route matches, the route matching process stops. The :term:`request`
is decorated with a special :term:`interface` which describes it as a "route
request", the :term:`context` resource is generated, and the context and the
resulting request are handed off to :term:`view lookup`. During view lookup,
-if any ``view`` argument was provided within the matched route configuration,
-the :term:`view callable` it points to is called.
+if a :term:`view callable` associated with the matched route is found, that
+view is called.
When a route configuration is declared, it may contain :term:`route
predicate` arguments. All route predicates associated with a route
@@ -621,7 +667,8 @@ result in a particular view callable being invoked:
.. code-block:: python
:linenos:
- config.add_route('idea', 'site/{id}', view='mypackage.views.site_view')
+ config.add_route('idea', 'site/{id}')
+ config.add_view('mypackage.views.site_view', route_name='idea')
When a route configuration with a ``view`` attribute is added to the system,
and an incoming request matches the *pattern* of the route configuration, the
@@ -665,9 +712,13 @@ add to your application:
.. code-block:: python
:linenos:
- config.add_route('idea', 'ideas/{idea}', view='mypackage.views.idea_view')
- config.add_route('user', 'users/{user}', view='mypackage.views.user_view')
- config.add_route('tag', 'tags/{tags}', view='mypackage.views.tag_view')
+ config.add_route('idea', 'ideas/{idea}')
+ config.add_route('user', 'users/{user}')
+ config.add_route('tag', 'tags/{tags}')
+
+ config.add_view('mypackage.views.idea_view', route_name='idea')
+ config.add_view('mypackage.views.user_view', route_name='user')
+ config.add_view('mypackage.views.tag_view', route_name='tag')
The above configuration will allow :app:`Pyramid` to service URLs in these
forms:
@@ -717,9 +768,8 @@ An example of using a route with a factory:
.. code-block:: python
:linenos:
- config.add_route('idea', 'ideas/{idea}',
- view='myproject.views.idea_view',
- factory='myproject.resources.Idea')
+ config.add_route('idea', 'ideas/{idea}', factory='myproject.resources.Idea')
+ config.add_view('myproject.views.idea_view', route_name='idea')
The above route will manufacture an ``Idea`` resource as a :term:`context`,
assuming that ``mypackage.resources.Idea`` resolves to a class that accepts a
@@ -735,34 +785,6 @@ request in its ``__init__``. For example:
In a more complicated application, this root factory might be a class
representing a :term:`SQLAlchemy` model.
-Example 4
-~~~~~~~~~
-
-It is possible to create a route declaration without a ``view`` attribute,
-but associate the route with a :term:`view callable` using a ``view``
-declaration.
-
-.. code-block:: python
- :linenos:
-
- config.add_route('idea', 'site/{id}')
- config.add_view(route_name='idea', view='mypackage.views.site_view')
-
-This set of configuration parameters creates a configuration completely
-equivalent to this example provided in :ref:`urldispatch_example1`:
-
-.. code-block:: python
- :linenos:
-
- config.add_route('idea', 'site/{id}', view='mypackage.views.site_view')
-
-In fact, the spelling which names a ``view`` attribute is just syntactic
-sugar for the more verbose spelling which contains separate view and route
-registrations.
-
-More uses for this style of associating views with routes are explored in
-:ref:`hybrid_chapter`.
-
.. index::
single: matching the root URL
single: root url (matching)
@@ -777,14 +799,14 @@ It's not entirely obvious how to use a route pattern to match the root URL
.. code-block:: python
:linenos:
- config.add_route('root', '', view='mypackage.views.root_view')
+ config.add_route('root', '')
Or provide the literal string ``/`` as the pattern:
.. code-block:: python
:linenos:
- config.add_route('root', '/', view='mypackage.views.root_view')
+ config.add_route('root', '/')
.. index::
single: generating route URLs
@@ -834,10 +856,11 @@ route configuration looks like so:
.. code-block:: python
:linenos:
- config.add_route('noslash', 'no_slash',
- view='myproject.views.no_slash')
- config.add_route('hasslash', 'has_slash/',
- view='myproject.views.has_slash')
+ config.add_route('noslash', 'no_slash')
+ config.add_route('hasslash', 'has_slash/')
+
+ config.add_view('myproject.views.no_slash', route_name='noslash')
+ config.add_view('myproject.views.has_slash', route_name='hasslash')
If a request enters the application with the ``PATH_INFO`` value of
``/has_slash/``, the second route will match. If a request enters the
@@ -864,8 +887,8 @@ the application's startup configuration, adding the following stanza:
.. code-block:: python
:linenos:
- config.add_view(context='pyramid.exceptions.NotFound',
- view='pyramid.view.append_slash_notfound_view')
+ config.add_view('pyramid.view.append_slash_notfound_view',
+ context='pyramid.exceptions.NotFound')
See :ref:`view_module` and :ref:`changing_the_notfound_view` for more
information about the slash-appending not found view and for a more general
@@ -1063,30 +1086,25 @@ is executed.
Route View Callable Registration and Lookup Details
---------------------------------------------------
-The purpose of making it possible to specify a view callable within a route
-configuration is to prevent developers from needing to deeply understand the
-details of :term:`resource location` and :term:`view lookup`. When a route
-names a view callable as a ``view`` argument, and a request enters the system
-which matches the pattern of the route, the result is simple: the view
-callable associated with the route is invoked with the request that caused
-the invocation.
+When a request enters the system which matches the pattern of the route, the
+usual result is simple: the view callable associated with the route is
+invoked with the request that caused the invocation.
For most usage, you needn't understand more than this; how it works is an
implementation detail. In the interest of completeness, however, we'll
explain how it *does* work in the this section. You can skip it if you're
uninterested.
-When a ``view`` attribute is attached to a route configuration,
-:app:`Pyramid` ensures that a :term:`view configuration` is registered that
-will always be found when the route pattern is matched during a request. To
-do so:
+When a view is associated with a route configuration, :app:`Pyramid` ensures
+that a :term:`view configuration` is registered that will always be found
+when the route pattern is matched during a request. To do so:
- A special route-specific :term:`interface` is created at startup time for
each route configuration declaration.
-- When a route configuration declaration mentions a ``view`` attribute, a
+- When an ``add_view`` statement mentions a ``route name`` attribute, a
:term:`view configuration` is registered at startup time. This view
- configuration uses the route-specific interface as a :term:`request` type.
+ configuration uses a route-specific interface as a :term:`request` type.
- At runtime, when a request causes any route to match, the :term:`request`
object is decorated with the route-specific interface.
diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst
index 9b2500a2b..743cc016e 100644
--- a/docs/narr/viewconfig.rst
+++ b/docs/narr/viewconfig.rst
@@ -59,7 +59,8 @@ View configuration is performed in one of these ways:
- By specifying a view within a :term:`route configuration`. View
configuration via a route configuration is performed by using the
:meth:`pyramid.config.Configurator.add_route` method, passing a ``view``
- argument specifying a view callable.
+ argument specifying a view callable. This pattern of view configuration is
+ deprecated as of :app:`Pyramid` 1.1.
.. note:: A package named ``pyramid_handlers`` (available from PyPI) provides
an analogue of :term:`Pylons` -style "controllers", which are a special
diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst
index aa0e03599..19d438fad 100644
--- a/docs/tutorials/wiki2/authorization.rst
+++ b/docs/tutorials/wiki2/authorization.rst
@@ -104,7 +104,7 @@ We'll also change ``__init__.py``, adding a call to
:term:`view callable`. This is also known as a :term:`forbidden view`:
.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 24-26,40-42
+ :lines: 24-26,41-43
:linenos:
:language: python
@@ -118,18 +118,18 @@ A ``logout`` :term:`view callable` will allow users to log out later:
:linenos:
:language: python
-We'll also add ``view_permission`` arguments with the value ``edit`` to the
-``edit_page`` and ``add_page`` routes. This indicates that the view
-callables which these routes reference cannot be invoked without the
+We'll also add ``permission`` arguments with the value ``edit`` to the
+``edit_page`` and ``add_page`` views. This indicates that the view
+callables which these views reference cannot be invoked without the
authenticated user possessing the ``edit`` permission with respect to the
current context.
.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 32-39
+ :lines: 37-40
:linenos:
:language: python
-Adding these ``view_permission`` arguments causes Pyramid to make the
+Adding these ``permission`` arguments causes Pyramid to make the
assertion that only users who possess the effective ``edit`` permission at
the time of the request may invoke those two views. We've granted the
``group:editors`` principal the ``edit`` permission at the root model via its
diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst
index 9bd65e8b8..82e112c64 100644
--- a/docs/tutorials/wiki2/basiclayout.rst
+++ b/docs/tutorials/wiki2/basiclayout.rst
@@ -81,28 +81,34 @@ via the :meth:`pyramid.config.Configurator.add_route` method that will be
used when the URL is ``/``:
.. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 13-14
+ :lines: 13
:language: py
Since this route has a ``pattern`` equalling ``/`` it is the route that will
-be called when the URL ``/`` is visted, e.g. ``http://localhost:6543/``. The
-argument named ``view`` with the value ``tutorial.views.my_view`` is the
+be matched when the URL ``/`` is visted, e.g. ``http://localhost:6543/``.
+
+Mapping the ``home`` route to code is done by registering a view. You will
+use :meth:`pyramid.config.Configurator.add_view` in :term:`URL dispatch` to
+register views for the routes, mapping your patterns to code:
+
+ .. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :lines: 14
+ :language: py
+
+The first positional ``add_view`` argument ``tutorial.views.my_view`` is the
dotted name to a *function* we write (generated by the
``pyramid_routesalchemy`` scaffold) that is given a ``request`` object and
-which returns a response or a dictionary.
-
-You will use :meth:`pyramid.config.Configurator.add_route` statements in a
-:term:`URL dispatch` based application to map URLs to code. This route also
-names a ``view_renderer``, which is a template which lives in the
-``templates`` subdirectory of the package. When the
-``tutorial.views.my_view`` view returns a dictionary, a :term:`renderer` will
-use this template to create a response.
+which returns a response or a dictionary. This view also names a
+``renderer``, which is a template which lives in the ``templates``
+subdirectory of the package. When the ``tutorial.views.my_view`` view
+returns a dictionary, a :term:`renderer` will use this template to create a
+response. This
Finally, we use the :meth:`pyramid.config.Configurator.make_wsgi_app`
method to return a :term:`WSGI` application:
.. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 15
+ :lines: 16
:language: py
Our final ``__init__.py`` file will look like this:
diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst
index c5a452d11..832f90b92 100644
--- a/docs/tutorials/wiki2/definingviews.rst
+++ b/docs/tutorials/wiki2/definingviews.rst
@@ -272,8 +272,8 @@ Mapping Views to URLs in ``__init__.py``
========================================
The ``__init__.py`` file contains
-:meth:`pyramid.config.Configurator.add_route` calls which serve to map
-URLs via :term:`url dispatch` to view functions. First, we’ll get rid of the
+:meth:`pyramid.config.Configurator.add_view` calls which serve to map
+routes via :term:`url dispatch` to views. First, we’ll get rid of the
existing route created by the template using the name ``home``. It’s only an
example and isn’t relevant to our application.
@@ -282,21 +282,33 @@ these declarations is very important. ``route`` declarations are matched in
the order they're found in the ``__init__.py`` file.
#. Add a declaration which maps the pattern ``/`` (signifying the root URL)
- to the view named ``view_wiki`` in our ``views.py`` file with the name
- ``view_wiki``. This is the :term:`default view` for the wiki.
+ to the route named ``view_wiki``.
-#. Add a declaration which maps the pattern ``/{pagename}`` to the view named
- ``view_page`` in our ``views.py`` file with the view name ``view_page``.
- This is the regular view for a page.
+#. Add a declaration which maps the pattern ``/{pagename}`` to the route named
+ ``view_page``. This is the regular view for a page.
-#. Add a declaration which maps the pattern
- ``/add_page/{pagename}`` to the view named ``add_page`` in our
- ``views.py`` file with the name ``add_page``. This is the add view
- for a new page.
+#. Add a declaration which maps the pattern ``/add_page/{pagename}`` to the
+ route named ``add_page``. This is the add view for a new page.
#. Add a declaration which maps the pattern ``/{pagename}/edit_page`` to the
- view named ``edit_page`` in our ``views.py`` file with the name
- ``edit_page``. This is the edit view for a page.
+ route named ``edit_page``. This is the edit view for a page.
+
+After we've defined the routes for our application, we can register views
+to handle the processing and rendering that needs to happen when each route is
+requested.
+
+#. Add a declaration which maps the ``view_wiki`` route to the view named
+ ``view_wiki`` in our ``views.py`` file. This is the :term:`default view`
+ for the wiki.
+
+#. Add a declaration which maps the ``view_page`` route to the view named
+ ``view_page`` in our ``views.py`` file.
+
+#. Add a declaration which maps the ``add_page`` route to the view named
+ ``add_page`` in our ``views.py`` file.
+
+#. Add a declaration which maps the ``edit_page`` route to the view named
+ ``edit_page`` in our ``views.py`` file.
As a result of our edits, the ``__init__.py`` file should look
something like so:
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py
index 025b94927..e8baa568c 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py
@@ -20,25 +20,26 @@ def main(global_config, **settings):
authentication_policy=authn_policy,
authorization_policy=authz_policy)
config.add_static_view('static', 'tutorial:static')
- config.add_route('view_wiki', '/', view='tutorial.views.view_wiki')
- config.add_route('login', '/login',
- view='tutorial.login.login',
- view_renderer='tutorial:templates/login.pt')
- config.add_route('logout', '/logout',
- view='tutorial.login.logout')
- config.add_route('view_page', '/{pagename}',
- view='tutorial.views.view_page',
- view_renderer='tutorial:templates/view.pt')
- config.add_route('add_page', '/add_page/{pagename}',
- view='tutorial.views.add_page',
- view_renderer='tutorial:templates/edit.pt',
- view_permission='edit')
- config.add_route('edit_page', '/{pagename}/edit_page',
- view='tutorial.views.edit_page',
- view_renderer='tutorial:templates/edit.pt',
- view_permission='edit')
+
+ config.add_route('view_wiki', '/')
+ config.add_route('login', '/login')
+ config.add_route('logout', '/logout')
+ config.add_route('view_page', '/{pagename}')
+ config.add_route('add_page', '/add_page/{pagename}')
+ config.add_route('edit_page', '/{pagename}/edit_page')
+ config.add_route('view_wiki', '/')
+
+ config.add_view('tutorial.login.login', route_name='login',
+ renderer='tutorial:templates/login.pt')
+ config.add_view('tutorial.login.logout', route_name='logout')
+ config.add_view('tutorial.views.view_page', route_name='view_page',
+ renderer='tutorial:templates/view.pt')
+ config.add_view('tutorial.views.add_page', route_name='add_page',
+ renderer='tutorial:templates/edit.pt', permission='edit')
+ config.add_view('tutorial.views.edit_page', route_name='edit_page',
+ renderer='tutorial:templates/edit.pt', permission='edit')
config.add_view('tutorial.login.login',
- renderer='tutorial:templates/login.pt',
- context='pyramid.exceptions.Forbidden')
+ context='pyramid.exceptions.Forbidden',
+ renderer='tutorial:templates/login.pt')
return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py
index d27b891c0..c74f07652 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py
@@ -10,8 +10,9 @@ def main(global_config, **settings):
initialize_sql(engine)
config = Configurator(settings=settings)
config.add_static_view('static', 'tutorial:static')
- config.add_route('home', '/', view='tutorial.views.my_view',
- view_renderer='templates/mytemplate.pt')
+ config.add_route('home', '/')
+ config.add_view('tutorial.views.my_view', route_name='home',
+ renderer='templates/mytemplate.pt')
return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/models/tutorial/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/__init__.py
index c912a015b..ecc41ca9f 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/__init__.py
+++ b/docs/tutorials/wiki2/src/models/tutorial/__init__.py
@@ -10,6 +10,7 @@ def main(global_config, **settings):
initialize_sql(engine)
config = Configurator(settings=settings)
config.add_static_view('static', 'tutorial:static')
- config.add_route('home', '/', view='tutorial.views.my_view',
- view_renderer='templates/mytemplate.pt')
+ config.add_route('home', '/')
+ config.add_view('tutorial.views.my_view', route_name='home',
+ renderer='templates/mytemplate.pt')
return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/views/tutorial/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/__init__.py
index 1a8d24499..ad89c124e 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/__init__.py
+++ b/docs/tutorials/wiki2/src/views/tutorial/__init__.py
@@ -10,15 +10,16 @@ def main(global_config, **settings):
initialize_sql(engine)
config = Configurator(settings=settings)
config.add_static_view('static', 'tutorial:static')
- config.add_route('view_wiki', '/', view='tutorial.views.view_wiki')
- config.add_route('view_page', '/{pagename}',
- view='tutorial.views.view_page',
- view_renderer='tutorial:templates/view.pt')
- config.add_route('add_page', '/add_page/{pagename}',
- view='tutorial.views.add_page',
- view_renderer='tutorial:templates/edit.pt')
- config.add_route('edit_page', '/{pagename}/edit_page',
- view='tutorial.views.edit_page',
- view_renderer='tutorial:templates/edit.pt')
+ config.add_route('view_wiki', '/')
+ config.add_route('view_page', '/{pagename}')
+ config.add_route('add_page', '/add_page/{pagename}')
+ config.add_route('edit_page', '/{pagename}/edit_page')
+ config.add_view('tutorial.views.view_wiki', route_name='view_wiki')
+ config.add_view('tutorial.views.view_page', route_name='view_page',
+ renderer='tutorial:templates/view.pt')
+ config.add_view('tutorial.views.add_page', route_name='add_page',
+ renderer='tutorial:templates/edit.pt')
+ config.add_view('tutorial.views.edit_page', route_name='edit_page',
+ renderer='tutorial:templates/edit.pt')
return config.make_wsgi_app()
diff --git a/pyramid/compat/__init__.py b/pyramid/compat/__init__.py
index 8bbf79703..096fb3ddf 100644
--- a/pyramid/compat/__init__.py
+++ b/pyramid/compat/__init__.py
@@ -146,3 +146,12 @@ except ImportError: # pragma: no cover
import md5
md5 = md5.new
+try:
+ any = any # make importable
+except NameError: # pragma: no cover
+ def any(L):
+ for thing in L:
+ if thing:
+ return True
+ return False
+
diff --git a/pyramid/config.py b/pyramid/config.py
index 9fda75daa..0041a6726 100644
--- a/pyramid/config.py
+++ b/pyramid/config.py
@@ -4,6 +4,7 @@ import re
import sys
import types
import traceback
+import warnings
import venusian
@@ -50,6 +51,7 @@ from pyramid import renderers
from pyramid.authorization import ACLAuthorizationPolicy
from pyramid.compat import all
from pyramid.compat import md5
+from pyramid.compat import any
from pyramid.events import ApplicationCreated
from pyramid.exceptions import ConfigurationError
from pyramid.exceptions import Forbidden
@@ -1378,6 +1380,52 @@ class Configurator(object):
discriminator = tuple(discriminator)
self.action(discriminator, register)
+ def _add_view_from_route(self,
+ route_name,
+ view,
+ context,
+ permission,
+ renderer,
+ attr,
+ ):
+ if view:
+ self.add_view(
+ permission=permission,
+ context=context,
+ view=view,
+ name='',
+ route_name=route_name,
+ renderer=renderer,
+ attr=attr,
+ )
+ else:
+ # prevent mistakes due to misunderstanding of how hybrid calls to
+ # add_route and add_view interact
+ if attr:
+ raise ConfigurationError(
+ 'view_attr argument not permitted without view '
+ 'argument')
+ if context:
+ raise ConfigurationError(
+ 'view_context argument not permitted without view '
+ 'argument')
+ if permission:
+ raise ConfigurationError(
+ 'view_permission argument not permitted without view '
+ 'argument')
+ if renderer:
+ raise ConfigurationError(
+ 'view_renderer argument not permitted without '
+ 'view argument')
+
+ warnings.warn(
+ 'Passing view-related arguments to add_route() is deprecated as of '
+ 'Pyramid 1.1. Use add_view() to associate a view with a route '
+ 'instead. See "Deprecations" in "What\'s New in Pyramid 1.1" '
+ 'within the general Pyramid documentation for further details.',
+ DeprecationWarning,
+ 4)
+
@action_method
def add_route(self,
name,
@@ -1484,6 +1532,14 @@ class Configurator(object):
by applications, it is meant to be hooked by frameworks
that use :app:`Pyramid` as a base.
+ use_global_views
+
+ When a request matches this route, and view lookup cannot
+ find a view which has a ``route_name`` predicate argument
+ that matches the route, try to fall back to using a view
+ that otherwise matches the context, request, and view name
+ (but which does not match the route_name predicate).
+
Predicate Arguments
pattern
@@ -1590,16 +1646,31 @@ class Configurator(object):
:ref:`custom_route_predicates` for more information about
``info``.
+ .. _add_route_view_related_api:
+
View-Related Arguments
+ .. warning:: The arguments described below have been deprecated as of
+ :app:`Pyramid` 1.1. *Do not use these for new development; they
+ should only be used to support older code bases which depend upon
+ them.* Use a separate call to
+ :meth:`pyramid.config.Configurator.add_view` to associate a view
+ with a route.
+
view
+ .. warning:: Deprecated as of :app:`Pyramid` 1.1; see
+ :ref:`add_route_view_related_api`.
+
A Python object or :term:`dotted Python name` to the same
object that will be used as a view callable when this route
matches. e.g. ``mypackage.views.my_view``.
view_context
+ .. warning:: Deprecated as of :app:`Pyramid` 1.1; see
+ :ref:`add_route_view_related_api`.
+
A class or an :term:`interface` or :term:`dotted Python
name` to the same object which the :term:`context` of the
view should match for the view named by the route to be
@@ -1614,6 +1685,9 @@ class Configurator(object):
view_permission
+ .. warning:: Deprecated as of :app:`Pyramid` 1.1; see
+ :ref:`add_route_view_related_api`.
+
The permission name required to invoke the view associated
with this route. e.g. ``edit``. (see
:ref:`using_security_with_urldispatch` for more information
@@ -1626,6 +1700,9 @@ class Configurator(object):
view_renderer
+ .. warning:: Deprecated as of :app:`Pyramid` 1.1; see
+ :ref:`add_route_view_related_api`.
+
This is either a single string term (e.g. ``json``) or a
string implying a path or :term:`asset specification`
(e.g. ``templates/views.pt``). If the renderer value is a
@@ -1648,6 +1725,9 @@ class Configurator(object):
view_attr
+ .. warning:: Deprecated as of :app:`Pyramid` 1.1; see
+ :ref:`add_route_view_related_api`.
+
The view machinery defaults to using the ``__call__`` method
of the view callable (or the function itself, if the view
callable is a function) to obtain a response dictionary.
@@ -1662,14 +1742,6 @@ class Configurator(object):
If the ``view`` argument is not provided, this argument has no
effect.
- use_global_views
-
- When a request matches this route, and view lookup cannot
- find a view which has a ``route_name`` predicate argument
- that matches the route, try to fall back to using a view
- that otherwise matches the context, request, and view name
- (but which does not match the route_name predicate).
-
"""
# these are route predicates; if they do not match, the next route
# in the routelist will be tried
@@ -1698,42 +1770,17 @@ class Configurator(object):
for info in view_info:
self.add_view(**info)
- if view_context is None:
- view_context = view_for
- if view_context is None:
- view_context = for_
- view_permission = view_permission or permission
- view_renderer = view_renderer or renderer
-
- if view:
- self.add_view(
- permission=view_permission,
- context=view_context,
- view=view,
- name='',
+ # deprecated adding views from add_route
+ if any([view, view_context, view_permission, view_renderer,
+ view_for, for_, permission, renderer, view_attr]):
+ self._add_view_from_route(
route_name=name,
- renderer=view_renderer,
+ view=view,
+ permission=view_permission or permission,
+ context=view_context or view_for or for_,
+ renderer=view_renderer or renderer,
attr=view_attr,
- )
- else:
- # prevent mistakes due to misunderstanding of how hybrid calls to
- # add_route and add_view interact
- if view_attr:
- raise ConfigurationError(
- 'view_attr argument not permitted without view '
- 'argument')
- if view_context:
- raise ConfigurationError(
- 'view_context argument not permitted without view '
- 'argument')
- if view_permission:
- raise ConfigurationError(
- 'view_permission argument not permitted without view '
- 'argument')
- if view_renderer:
- raise ConfigurationError(
- 'view_renderer argument not permitted without '
- 'view argument')
+ )
mapper = self.get_routes_mapper()
diff --git a/pyramid/paster_templates/routesalchemy/+package+/__init__.py_tmpl b/pyramid/paster_templates/routesalchemy/+package+/__init__.py_tmpl
index 479740297..f5e3a0630 100644
--- a/pyramid/paster_templates/routesalchemy/+package+/__init__.py_tmpl
+++ b/pyramid/paster_templates/routesalchemy/+package+/__init__.py_tmpl
@@ -10,8 +10,9 @@ def main(global_config, **settings):
initialize_sql(engine)
config = Configurator(settings=settings)
config.add_static_view('static', '{{package}}:static')
- config.add_route('home', '/', view='{{package}}.views.my_view',
- view_renderer='templates/mytemplate.pt')
+ config.add_route('home', '/')
+ config.add_view('{{package}}.views.my_view',
+ route_name='home',
+ renderer='templates/mytemplate.pt')
return config.make_wsgi_app()
-
diff --git a/pyramid/request.py b/pyramid/request.py
index 9d2b9344b..0fe8b9379 100644
--- a/pyramid/request.py
+++ b/pyramid/request.py
@@ -10,6 +10,7 @@ from pyramid.interfaces import IResponseFactory
from pyramid.exceptions import ConfigurationError
from pyramid.decorator import reify
from pyramid.response import Response
+from pyramid.traversal import quote_path_segment
from pyramid.url import resource_url
from pyramid.url import route_url
from pyramid.url import static_url
@@ -393,3 +394,54 @@ def add_global_response_headers(request, headerlist):
response.headerlist.append((k, v))
request.add_response_callback(add_headers)
+def call_app_with_subpath_as_path_info(request, app):
+ # Copy the request. Use the source request's subpath (if it exists) as
+ # the new request's PATH_INFO. Set the request copy's SCRIPT_NAME to the
+ # prefix before the subpath. Call the application with the new request
+ # and return a response.
+ #
+ # Postconditions:
+ # - SCRIPT_NAME and PATH_INFO are empty or start with /
+ # - At least one of SCRIPT_NAME or PATH_INFO are set.
+ # - SCRIPT_NAME is not '/' (it should be '', and PATH_INFO should
+ # be '/').
+
+ environ = request.environ
+ script_name = environ.get('SCRIPT_NAME', '')
+ path_info = environ.get('PATH_INFO', '/')
+ subpath = list(getattr(request, 'subpath', ()))
+
+ new_script_name = ''
+
+ # compute new_path_info
+ new_path_info = '/' + '/'.join([x.encode('utf-8') for x in subpath])
+
+ if new_path_info != '/': # don't want a sole double-slash
+ if path_info != '/': # if orig path_info is '/', we're already done
+ if path_info.endswith('/'):
+ # readd trailing slash stripped by subpath (traversal)
+ # conversion
+ new_path_info += '/'
+
+ # compute new_script_name
+ workback = (script_name + path_info).split('/')
+
+ tmp = []
+ while workback:
+ if tmp == subpath:
+ break
+ el = workback.pop()
+ if el:
+ tmp.insert(0, el.decode('utf-8'))
+
+ # strip all trailing slashes from workback to avoid appending undue slashes
+ # to end of script_name
+ while workback and (workback[-1] == ''):
+ workback = workback[:-1]
+
+ new_script_name = '/'.join(workback)
+
+ new_request = request.copy()
+ new_request.environ['SCRIPT_NAME'] = new_script_name
+ new_request.environ['PATH_INFO'] = new_path_info
+ return new_request.get_response(app)
diff --git a/pyramid/static.py b/pyramid/static.py
index 223652768..ec7b4cb00 100644
--- a/pyramid/static.py
+++ b/pyramid/static.py
@@ -13,6 +13,7 @@ from zope.interface import implements
from pyramid.asset import resolve_asset_spec
from pyramid.interfaces import IStaticURLInfo
from pyramid.path import caller_package
+from pyramid.request import call_app_with_subpath_as_path_info
from pyramid.url import route_url
class PackageURLParser(StaticURLParser):
@@ -148,13 +149,26 @@ class StaticURLInfo(object):
permission = extra.pop('permission', None)
if permission is None:
permission = '__no_permission_required__'
- extra['view_permission'] = permission
- extra['view'] = view
+
+ context = extra.pop('view_context', None)
+ if context is None:
+ context = extra.pop('view_for', None)
+ if context is None:
+ context = extra.pop('for_', None)
+
+ renderer = extra.pop('view_renderer', None)
+ if renderer is None:
+ renderer = extra.pop('renderer', None)
+
+ attr = extra.pop('view_attr', None)
# register a route using the computed view, permission, and
# pattern, plus any extras passed to us via add_static_view
pattern = "%s*subpath" % name # name already ends with slash
self.config.add_route(name, pattern, **extra)
+ self.config.add_view(route_name=name, view=view,
+ permission=permission, context=context,
+ renderer=renderer, attr=attr)
self.registrations.append((name, spec, False))
class static_view(object):
@@ -208,44 +222,4 @@ class static_view(object):
self.app = app
def __call__(self, context, request):
- # Point PATH_INFO to the static file/dir path; point SCRIPT_NAME
- # to the prefix before it.
-
- # Postconditions:
- # - SCRIPT_NAME and PATH_INFO are empty or start with /
- # - At least one of SCRIPT_NAME or PATH_INFO are set.
- # - SCRIPT_NAME is not '/' (it should be '', and PATH_INFO should
- # be '/').
-
- request_copy = request.copy()
- script_name = request_copy.environ.get('SCRIPT_NAME', '')
- path_info = request_copy.environ.get('PATH_INFO', '/')
-
- new_script_name = script_name
- new_path_info = path_info
-
- subpath = list(request.subpath)
-
- if subpath:
- # compute new_path_info
- new_path_info = '/' + '/'.join(subpath)
- if path_info.endswith('/'):
- # readd trailing slash stripped by subpath (traversal)
- # conversion
- new_path_info += '/'
-
- # compute new_script_name
- tmp = []
- workback = (script_name + path_info).split('/')
- while workback:
- el = workback.pop()
- if el:
- tmp.insert(0, el)
- if tmp == subpath:
- new_script_name = '/'.join(workback)
- break
-
- request_copy.environ['SCRIPT_NAME'] = new_script_name
- request_copy.environ['PATH_INFO'] = new_path_info
-
- return request_copy.get_response(self.app)
+ return call_app_with_subpath_as_path_info(request, self.app)
diff --git a/pyramid/tests/ccbugapp/__init__.py b/pyramid/tests/ccbugapp/__init__.py
index 6c2eb6ecf..ad6387a75 100644
--- a/pyramid/tests/ccbugapp/__init__.py
+++ b/pyramid/tests/ccbugapp/__init__.py
@@ -1,7 +1,8 @@
def includeme(config):
- config.add_route('rdf',
- 'licenses/:license_code/:license_version/rdf',
- '.views.rdf_view')
+ config.add_route('rdf', 'licenses/:license_code/:license_version/rdf')
config.add_route('juri',
- 'licenses/:license_code/:license_version/:jurisdiction',
- '.views.juri_view')
+ 'licenses/:license_code/:license_version/:jurisdiction')
+ config.add_view('.views.rdf_view', route_name='rdf')
+ config.add_view('.views.juri_view', route_name='juri')
+
+
diff --git a/pyramid/tests/exceptionviewapp/__init__.py b/pyramid/tests/exceptionviewapp/__init__.py
index cf69227cd..f169e0cd5 100644
--- a/pyramid/tests/exceptionviewapp/__init__.py
+++ b/pyramid/tests/exceptionviewapp/__init__.py
@@ -1,21 +1,23 @@
def includeme(config):
+ config.add_route('route_raise_exception', 'route_raise_exception')
+ config.add_route('route_raise_exception2', 'route_raise_exception2',
+ factory='.models.route_factory')
+ config.add_route('route_raise_exception3', 'route_raise_exception3',
+ factory='.models.route_factory2')
+ config.add_route('route_raise_exception4', 'route_raise_exception4')
config.add_view('.views.maybe')
config.add_view('.views.no', context='.models.NotAnException')
config.add_view('.views.yes', context=".models.AnException")
config.add_view('.views.raise_exception', name='raise_exception')
- config.add_route('route_raise_exception', 'route_raise_exception',
- view='.views.raise_exception')
- config.add_route('route_raise_exception2',
- 'route_raise_exception2',
- view='.views.raise_exception',
- factory='.models.route_factory')
- config.add_route('route_raise_exception3',
- 'route_raise_exception3',
- view='.views.raise_exception',
- factory='.models.route_factory2')
+ config.add_view('.views.raise_exception',
+ route_name='route_raise_exception')
+ config.add_view('.views.raise_exception',
+ route_name='route_raise_exception2')
+ config.add_view('.views.raise_exception',
+ route_name='route_raise_exception3')
config.add_view('.views.whoa', context='.models.AnException',
route_name='route_raise_exception3')
- config.add_route('route_raise_exception4', 'route_raise_exception4',
- view='.views.raise_exception')
+ config.add_view('.views.raise_exception',
+ route_name='route_raise_exception4')
config.add_view('.views.whoa', context='.models.AnException',
route_name='route_raise_exception4')
diff --git a/pyramid/tests/hybridapp/__init__.py b/pyramid/tests/hybridapp/__init__.py
index 5b51e3d1e..1cc2dde83 100644
--- a/pyramid/tests/hybridapp/__init__.py
+++ b/pyramid/tests/hybridapp/__init__.py
@@ -1,6 +1,7 @@
def includeme(config):
# <!-- we want this view to "win" -->
- config.add_route('route', 'abc', view='.views.route_view')
+ config.add_route('route', 'abc')
+ config.add_view('.views.route_view', route_name='route')
# <!-- .. even though this one has a more specific context -->
config.add_view('.views.global_view',
context='pyramid.traversal.DefaultRootFactory')
diff --git a/pyramid/tests/restbugapp/__init__.py b/pyramid/tests/restbugapp/__init__.py
index 461fcce92..9ad79e32e 100644
--- a/pyramid/tests/restbugapp/__init__.py
+++ b/pyramid/tests/restbugapp/__init__.py
@@ -1,14 +1,15 @@
def includeme(config):
config.add_route('gameactions_pet_get_pets', '/pet',
- view='.views.PetRESTView',
- view_attr='GET',
- request_method='GET',
- permission='view',
- renderer='json')
+ request_method='GET')
config.add_route('gameactions_pet_care_for_pet', '/pet',
- view='.views.PetRESTView',
- view_attr='POST',
- request_method='POST',
- permission='view',
- renderer='json')
-
+ request_method='POST')
+ config.add_view('.views.PetRESTView',
+ route_name='gameactions_pet_get_pets',
+ attr='GET',
+ permission='view',
+ renderer='json')
+ config.add_view('.views.PetRESTView',
+ route_name='gameactions_pet_care_for_pet',
+ attr='POST',
+ permission='view',
+ renderer='json')
diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py
index 5818f248b..560f68f95 100644
--- a/pyramid/tests/test_config.py
+++ b/pyramid/tests/test_config.py
@@ -68,6 +68,12 @@ class ConfiguratorTests(unittest.TestCase):
request.registry = config.registry
return request
+ def _conflictFunctions(self, e):
+ conflicts = e._conflicts.values()
+ for conflict in conflicts:
+ for confinst in conflict:
+ yield confinst[2]
+
def test_ctor_no_registry(self):
import sys
from pyramid.interfaces import ISettings
@@ -1979,129 +1985,6 @@ class ConfiguratorTests(unittest.TestCase):
request.accept = ['text/html']
self.assertEqual(predicate(None, request), False)
- def test_add_route_with_view(self):
- config = self._makeOne(autocommit=True)
- view = lambda *arg: 'OK'
- config.add_route('name', 'path', view=view)
- request_type = self._getRouteRequestIface(config, 'name')
- wrapper = self._getViewCallable(config, None, request_type)
- self.assertEqual(wrapper(None, None), 'OK')
- self._assertRoute(config, 'name', 'path')
-
- def test_add_route_with_view_context(self):
- config = self._makeOne(autocommit=True)
- view = lambda *arg: 'OK'
- config.add_route('name', 'path', view=view, view_context=IDummy)
- request_type = self._getRouteRequestIface(config, 'name')
- wrapper = self._getViewCallable(config, IDummy, request_type)
- self.assertEqual(wrapper(None, None), 'OK')
- self._assertRoute(config, 'name', 'path')
- wrapper = self._getViewCallable(config, IOther, request_type)
- self.assertEqual(wrapper, None)
-
- def test_add_route_with_view_exception(self):
- from zope.interface import implementedBy
- config = self._makeOne(autocommit=True)
- view = lambda *arg: 'OK'
- config.add_route('name', 'path', view=view, view_context=RuntimeError)
- request_type = self._getRouteRequestIface(config, 'name')
- wrapper = self._getViewCallable(
- config, ctx_iface=implementedBy(RuntimeError),
- request_iface=request_type, exception_view=True)
- self.assertEqual(wrapper(None, None), 'OK')
- self._assertRoute(config, 'name', 'path')
- wrapper = self._getViewCallable(
- config, ctx_iface=IOther,
- request_iface=request_type, exception_view=True)
- self.assertEqual(wrapper, None)
-
- def test_add_route_with_view_for(self):
- config = self._makeOne(autocommit=True)
- view = lambda *arg: 'OK'
- config.add_route('name', 'path', view=view, view_for=IDummy)
- request_type = self._getRouteRequestIface(config, 'name')
- wrapper = self._getViewCallable(config, IDummy, request_type)
- self.assertEqual(wrapper(None, None), 'OK')
- self._assertRoute(config, 'name', 'path')
- wrapper = self._getViewCallable(config, IOther, request_type)
- self.assertEqual(wrapper, None)
-
- def test_add_route_with_for_(self):
- config = self._makeOne(autocommit=True)
- view = lambda *arg: 'OK'
- config.add_route('name', 'path', view=view, for_=IDummy)
- request_type = self._getRouteRequestIface(config, 'name')
- wrapper = self._getViewCallable(config, IDummy, request_type)
- self.assertEqual(wrapper(None, None), 'OK')
- self._assertRoute(config, 'name', 'path')
- wrapper = self._getViewCallable(config, IOther, request_type)
- self.assertEqual(wrapper, None)
-
- def test_add_route_with_view_renderer(self):
- config = self._makeOne(autocommit=True)
- self._registerRenderer(config)
- view = lambda *arg: 'OK'
- config.add_route('name', 'path', view=view,
- view_renderer='fixtures/minimal.txt')
- request_type = self._getRouteRequestIface(config, 'name')
- wrapper = self._getViewCallable(config, None, request_type)
- self._assertRoute(config, 'name', 'path')
- self.assertEqual(wrapper(None, None).body, 'Hello!')
-
- def test_add_route_with_view_attr(self):
- config = self._makeOne(autocommit=True)
- self._registerRenderer(config)
- class View(object):
- def __init__(self, context, request):
- pass
- def alt(self):
- return 'OK'
- config.add_route('name', 'path', view=View, view_attr='alt')
- request_type = self._getRouteRequestIface(config, 'name')
- wrapper = self._getViewCallable(config, None, request_type)
- self._assertRoute(config, 'name', 'path')
- request = self._makeRequest(config)
- self.assertEqual(wrapper(None, request), 'OK')
-
- def test_add_route_with_view_renderer_alias(self):
- config = self._makeOne(autocommit=True)
- self._registerRenderer(config)
- view = lambda *arg: 'OK'
- config.add_route('name', 'path', view=view,
- renderer='fixtures/minimal.txt')
- request_type = self._getRouteRequestIface(config, 'name')
- wrapper = self._getViewCallable(config, None, request_type)
- self._assertRoute(config, 'name', 'path')
- self.assertEqual(wrapper(None, None).body, 'Hello!')
-
- def test_add_route_with_view_permission(self):
- from pyramid.interfaces import IAuthenticationPolicy
- from pyramid.interfaces import IAuthorizationPolicy
- config = self._makeOne(autocommit=True)
- policy = lambda *arg: None
- config.registry.registerUtility(policy, IAuthenticationPolicy)
- config.registry.registerUtility(policy, IAuthorizationPolicy)
- view = lambda *arg: 'OK'
- config.add_route('name', 'path', view=view, view_permission='edit')
- request_type = self._getRouteRequestIface(config, 'name')
- wrapper = self._getViewCallable(config, None, request_type)
- self._assertRoute(config, 'name', 'path')
- self.assertTrue(hasattr(wrapper, '__call_permissive__'))
-
- def test_add_route_with_view_permission_alias(self):
- from pyramid.interfaces import IAuthenticationPolicy
- from pyramid.interfaces import IAuthorizationPolicy
- config = self._makeOne(autocommit=True)
- policy = lambda *arg: None
- config.registry.registerUtility(policy, IAuthenticationPolicy)
- config.registry.registerUtility(policy, IAuthorizationPolicy)
- view = lambda *arg: 'OK'
- config.add_route('name', 'path', view=view, permission='edit')
- request_type = self._getRouteRequestIface(config, 'name')
- wrapper = self._getViewCallable(config, None, request_type)
- self._assertRoute(config, 'name', 'path')
- self.assertTrue(hasattr(wrapper, '__call_permissive__'))
-
def test_add_route_no_pattern_with_path(self):
config = self._makeOne(autocommit=True)
route = config.add_route('name', path='path')
@@ -3035,24 +2918,6 @@ class ConfiguratorTests(unittest.TestCase):
registeredview = self._getViewCallable(config)
self.assertEqual(registeredview.__name__, 'view3')
- def test_conflict_route_with_view(self):
- from zope.configuration.config import ConfigurationConflictError
- config = self._makeOne()
- def view1(request): pass
- def view2(request): pass
- config.add_route('a', '/a', view=view1)
- config.add_route('a', '/a', view=view2)
- try:
- config.commit()
- except ConfigurationConflictError, why:
- c1, c2, c3, c4 = self._conflictFunctions(why)
- self.assertEqual(c1, 'test_conflict_route_with_view')
- self.assertEqual(c2, 'test_conflict_route_with_view')
- self.assertEqual(c3, 'test_conflict_route_with_view')
- self.assertEqual(c4, 'test_conflict_route_with_view')
- else: # pragma: no cover
- raise AssertionError
-
def test_conflict_set_notfound_view(self):
from zope.configuration.config import ConfigurationConflictError
config = self._makeOne()
@@ -3106,12 +2971,6 @@ class ConfiguratorTests(unittest.TestCase):
self.assertTrue("@view_config(name='two', renderer='string')" in
which)
- def _conflictFunctions(self, e):
- conflicts = e._conflicts.values()
- for conflict in conflicts:
- for confinst in conflict:
- yield confinst[2]
-
def test___getattr__missing_when_directives_exist(self):
config = self._makeOne()
directives = {}
@@ -3138,6 +2997,217 @@ class ConfiguratorTests(unittest.TestCase):
foo_meth = config.foo
self.assertTrue(foo_meth.im_func is foo)
+class TestConfiguratorDeprecatedFeatures(unittest.TestCase):
+ def setUp(self):
+ import warnings
+ warnings.filterwarnings('ignore')
+
+ def tearDown(self):
+ import warnings
+ warnings.resetwarnings()
+
+ def _makeOne(self, *arg, **kw):
+ from pyramid.config import Configurator
+ return Configurator(*arg, **kw)
+
+ def _getRouteRequestIface(self, config, name):
+ from pyramid.interfaces import IRouteRequest
+ iface = config.registry.getUtility(IRouteRequest, name)
+ return iface
+
+ def _getViewCallable(self, config, ctx_iface=None, request_iface=None,
+ name='', exception_view=False):
+ from zope.interface import Interface
+ from pyramid.interfaces import IView
+ from pyramid.interfaces import IViewClassifier
+ from pyramid.interfaces import IExceptionViewClassifier
+ if exception_view:
+ classifier = IExceptionViewClassifier
+ else:
+ classifier = IViewClassifier
+ if ctx_iface is None:
+ ctx_iface = Interface
+ return config.registry.adapters.lookup(
+ (classifier, request_iface, ctx_iface), IView, name=name,
+ default=None)
+
+ def _registerRenderer(self, config, name='.txt'):
+ from pyramid.interfaces import IRendererFactory
+ from pyramid.interfaces import ITemplateRenderer
+ from zope.interface import implements
+ class Renderer:
+ implements(ITemplateRenderer)
+ def __init__(self, info):
+ self.__class__.info = info
+ def __call__(self, *arg):
+ return 'Hello!'
+ config.registry.registerUtility(Renderer, IRendererFactory, name=name)
+ return Renderer
+
+ def _assertRoute(self, config, name, path, num_predicates=0):
+ from pyramid.interfaces import IRoutesMapper
+ mapper = config.registry.getUtility(IRoutesMapper)
+ routes = mapper.get_routes()
+ route = routes[0]
+ self.assertEqual(len(routes), 1)
+ self.assertEqual(route.name, name)
+ self.assertEqual(route.path, path)
+ self.assertEqual(len(routes[0].predicates), num_predicates)
+ return route
+
+ def _makeRequest(self, config):
+ request = DummyRequest()
+ request.registry = config.registry
+ return request
+
+ def _conflictFunctions(self, e):
+ conflicts = e._conflicts.values()
+ for conflict in conflicts:
+ for confinst in conflict:
+ yield confinst[2]
+
+ def test_add_route_with_view(self):
+ config = self._makeOne(autocommit=True)
+ view = lambda *arg: 'OK'
+ config.add_route('name', 'path', view=view)
+ request_type = self._getRouteRequestIface(config, 'name')
+ wrapper = self._getViewCallable(config, None, request_type)
+ self.assertEqual(wrapper(None, None), 'OK')
+ self._assertRoute(config, 'name', 'path')
+
+ def test_add_route_with_view_context(self):
+ config = self._makeOne(autocommit=True)
+ view = lambda *arg: 'OK'
+ config.add_route('name', 'path', view=view, view_context=IDummy)
+ request_type = self._getRouteRequestIface(config, 'name')
+ wrapper = self._getViewCallable(config, IDummy, request_type)
+ self.assertEqual(wrapper(None, None), 'OK')
+ self._assertRoute(config, 'name', 'path')
+ wrapper = self._getViewCallable(config, IOther, request_type)
+ self.assertEqual(wrapper, None)
+
+ def test_add_route_with_view_exception(self):
+ from zope.interface import implementedBy
+ config = self._makeOne(autocommit=True)
+ view = lambda *arg: 'OK'
+ config.add_route('name', 'path', view=view, view_context=RuntimeError)
+ request_type = self._getRouteRequestIface(config, 'name')
+ wrapper = self._getViewCallable(
+ config, ctx_iface=implementedBy(RuntimeError),
+ request_iface=request_type, exception_view=True)
+ self.assertEqual(wrapper(None, None), 'OK')
+ self._assertRoute(config, 'name', 'path')
+ wrapper = self._getViewCallable(
+ config, ctx_iface=IOther,
+ request_iface=request_type, exception_view=True)
+ self.assertEqual(wrapper, None)
+
+ def test_add_route_with_view_for(self):
+ config = self._makeOne(autocommit=True)
+ view = lambda *arg: 'OK'
+ config.add_route('name', 'path', view=view, view_for=IDummy)
+ request_type = self._getRouteRequestIface(config, 'name')
+ wrapper = self._getViewCallable(config, IDummy, request_type)
+ self.assertEqual(wrapper(None, None), 'OK')
+ self._assertRoute(config, 'name', 'path')
+ wrapper = self._getViewCallable(config, IOther, request_type)
+ self.assertEqual(wrapper, None)
+
+ def test_add_route_with_for_(self):
+ config = self._makeOne(autocommit=True)
+ view = lambda *arg: 'OK'
+ config.add_route('name', 'path', view=view, for_=IDummy)
+ request_type = self._getRouteRequestIface(config, 'name')
+ wrapper = self._getViewCallable(config, IDummy, request_type)
+ self.assertEqual(wrapper(None, None), 'OK')
+ self._assertRoute(config, 'name', 'path')
+ wrapper = self._getViewCallable(config, IOther, request_type)
+ self.assertEqual(wrapper, None)
+
+ def test_add_route_with_view_renderer(self):
+ config = self._makeOne(autocommit=True)
+ self._registerRenderer(config)
+ view = lambda *arg: 'OK'
+ config.add_route('name', 'path', view=view,
+ view_renderer='fixtures/minimal.txt')
+ request_type = self._getRouteRequestIface(config, 'name')
+ wrapper = self._getViewCallable(config, None, request_type)
+ self._assertRoute(config, 'name', 'path')
+ self.assertEqual(wrapper(None, None).body, 'Hello!')
+
+ def test_add_route_with_view_attr(self):
+ config = self._makeOne(autocommit=True)
+ self._registerRenderer(config)
+ class View(object):
+ def __init__(self, context, request):
+ pass
+ def alt(self):
+ return 'OK'
+ config.add_route('name', 'path', view=View, view_attr='alt')
+ request_type = self._getRouteRequestIface(config, 'name')
+ wrapper = self._getViewCallable(config, None, request_type)
+ self._assertRoute(config, 'name', 'path')
+ request = self._makeRequest(config)
+ self.assertEqual(wrapper(None, request), 'OK')
+
+ def test_add_route_with_view_renderer_alias(self):
+ config = self._makeOne(autocommit=True)
+ self._registerRenderer(config)
+ view = lambda *arg: 'OK'
+ config.add_route('name', 'path', view=view,
+ renderer='fixtures/minimal.txt')
+ request_type = self._getRouteRequestIface(config, 'name')
+ wrapper = self._getViewCallable(config, None, request_type)
+ self._assertRoute(config, 'name', 'path')
+ self.assertEqual(wrapper(None, None).body, 'Hello!')
+
+ def test_add_route_with_view_permission(self):
+ from pyramid.interfaces import IAuthenticationPolicy
+ from pyramid.interfaces import IAuthorizationPolicy
+ config = self._makeOne(autocommit=True)
+ policy = lambda *arg: None
+ config.registry.registerUtility(policy, IAuthenticationPolicy)
+ config.registry.registerUtility(policy, IAuthorizationPolicy)
+ view = lambda *arg: 'OK'
+ config.add_route('name', 'path', view=view, view_permission='edit')
+ request_type = self._getRouteRequestIface(config, 'name')
+ wrapper = self._getViewCallable(config, None, request_type)
+ self._assertRoute(config, 'name', 'path')
+ self.assertTrue(hasattr(wrapper, '__call_permissive__'))
+
+ def test_add_route_with_view_permission_alias(self):
+ from pyramid.interfaces import IAuthenticationPolicy
+ from pyramid.interfaces import IAuthorizationPolicy
+ config = self._makeOne(autocommit=True)
+ policy = lambda *arg: None
+ config.registry.registerUtility(policy, IAuthenticationPolicy)
+ config.registry.registerUtility(policy, IAuthorizationPolicy)
+ view = lambda *arg: 'OK'
+ config.add_route('name', 'path', view=view, permission='edit')
+ request_type = self._getRouteRequestIface(config, 'name')
+ wrapper = self._getViewCallable(config, None, request_type)
+ self._assertRoute(config, 'name', 'path')
+ self.assertTrue(hasattr(wrapper, '__call_permissive__'))
+
+ def test_conflict_route_with_view(self):
+ from zope.configuration.config import ConfigurationConflictError
+ config = self._makeOne()
+ def view1(request): pass
+ def view2(request): pass
+ config.add_route('a', '/a', view=view1)
+ config.add_route('a', '/a', view=view2)
+ try:
+ config.commit()
+ except ConfigurationConflictError, why:
+ c1, c2, c3, c4 = self._conflictFunctions(why)
+ self.assertEqual(c1, 'test_conflict_route_with_view')
+ self.assertEqual(c2, 'test_conflict_route_with_view')
+ self.assertEqual(c3, 'test_conflict_route_with_view')
+ self.assertEqual(c4, 'test_conflict_route_with_view')
+ else: # pragma: no cover
+ raise AssertionError
+
+
class TestConfigurator_add_directive(unittest.TestCase):
def setUp(self):
diff --git a/pyramid/tests/test_integration.py b/pyramid/tests/test_integration.py
index 1f9484279..dd77d3aec 100644
--- a/pyramid/tests/test_integration.py
+++ b/pyramid/tests/test_integration.py
@@ -104,7 +104,7 @@ class TestStaticApp(unittest.TestCase):
from webob import Request
context = DummyContext()
from StringIO import StringIO
- request = Request({'PATH_INFO':'',
+ request = Request({'PATH_INFO':'/static',
'SCRIPT_NAME':'/script_name',
'SERVER_NAME':'localhost',
'SERVER_PORT':'80',
@@ -112,7 +112,7 @@ class TestStaticApp(unittest.TestCase):
'wsgi.version':(1,0),
'wsgi.url_scheme':'http',
'wsgi.input':StringIO()})
- request.subpath = ['static']
+ request.subpath = ('static',)
result = staticapp(context, request)
self.assertEqual(result.status, '301 Moved Permanently')
self.assertEqual(result.location,
@@ -390,6 +390,22 @@ class SelfScanAppTest(unittest.TestCase):
res = self.testapp.get('/two', status=200)
self.assertTrue('two' in res.body)
+class WSGIApp2AppTest(unittest.TestCase):
+ def setUp(self):
+ from pyramid.tests.wsgiapp2app import main
+ config = main()
+ app = config.make_wsgi_app()
+ from webtest import TestApp
+ self.testapp = TestApp(app)
+ self.config = config
+
+ def tearDown(self):
+ self.config.end()
+
+ def test_hello(self):
+ res = self.testapp.get('/hello', status=200)
+ self.assertTrue('Hello' in res.body)
+
class DummyContext(object):
pass
diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py
index 66a451220..60d59ece6 100644
--- a/pyramid/tests/test_request.py
+++ b/pyramid/tests/test_request.py
@@ -325,6 +325,66 @@ class Test_add_global_response_headers(unittest.TestCase):
request.response_callbacks[0](None, response)
self.assertEqual(response.headerlist, [('c', 1)] )
+class Test_call_app_with_subpath_as_path_info(unittest.TestCase):
+ def _callFUT(self, request, app):
+ from pyramid.request import call_app_with_subpath_as_path_info
+ return call_app_with_subpath_as_path_info(request, app)
+
+ def test_it_all_request_and_environment_data_missing(self):
+ request = DummyRequest({})
+ response = self._callFUT(request, 'app')
+ self.assertTrue(request.copied)
+ self.assertEqual(response, 'app')
+ self.assertEqual(request.environ['SCRIPT_NAME'], '')
+ self.assertEqual(request.environ['PATH_INFO'], '/')
+
+ def test_it_with_subpath_and_path_info(self):
+ request = DummyRequest({'PATH_INFO':'/hello'})
+ request.subpath = ('hello',)
+ response = self._callFUT(request, 'app')
+ self.assertTrue(request.copied)
+ self.assertEqual(response, 'app')
+ self.assertEqual(request.environ['SCRIPT_NAME'], '')
+ self.assertEqual(request.environ['PATH_INFO'], '/hello')
+
+ def test_it_with_subpath_and_path_info_path_info_endswith_slash(self):
+ request = DummyRequest({'PATH_INFO':'/hello/'})
+ request.subpath = ('hello',)
+ response = self._callFUT(request, 'app')
+ self.assertTrue(request.copied)
+ self.assertEqual(response, 'app')
+ self.assertEqual(request.environ['SCRIPT_NAME'], '')
+ self.assertEqual(request.environ['PATH_INFO'], '/hello/')
+
+ def test_it_with_subpath_and_path_info_extra_script_name(self):
+ request = DummyRequest({'PATH_INFO':'/hello', 'SCRIPT_NAME':'/script'})
+ request.subpath = ('hello',)
+ response = self._callFUT(request, 'app')
+ self.assertTrue(request.copied)
+ self.assertEqual(response, 'app')
+ self.assertEqual(request.environ['SCRIPT_NAME'], '/script')
+ self.assertEqual(request.environ['PATH_INFO'], '/hello')
+
+ def test_it_with_extra_slashes_in_path_info(self):
+ request = DummyRequest({'PATH_INFO':'//hello/',
+ 'SCRIPT_NAME':'/script'})
+ request.subpath = ('hello',)
+ response = self._callFUT(request, 'app')
+ self.assertTrue(request.copied)
+ self.assertEqual(response, 'app')
+ self.assertEqual(request.environ['SCRIPT_NAME'], '/script')
+ self.assertEqual(request.environ['PATH_INFO'], '/hello/')
+
+ def test_subpath_path_info_and_script_name_have_utf8(self):
+ la = 'La Pe\xc3\xb1a'
+ request = DummyRequest({'PATH_INFO':'/'+la, 'SCRIPT_NAME':'/'+la})
+ request.subpath = (unicode(la, 'utf-8'), )
+ response = self._callFUT(request, 'app')
+ self.assertTrue(request.copied)
+ self.assertEqual(response, 'app')
+ self.assertEqual(request.environ['SCRIPT_NAME'], '/' + la)
+ self.assertEqual(request.environ['PATH_INFO'], '/' + la)
+
class DummyRequest:
def __init__(self, environ=None):
if environ is None:
@@ -334,6 +394,13 @@ class DummyRequest:
def add_response_callback(self, callback):
self.response_callbacks = [callback]
+ def get_response(self, app):
+ return app
+
+ def copy(self):
+ self.copied = True
+ return self
+
class DummyResponse:
def __init__(self):
self.headerlist = []
diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py
index b9d464bf1..e7506628a 100644
--- a/pyramid/tests/test_static.py
+++ b/pyramid/tests/test_static.py
@@ -268,8 +268,9 @@ class Test_static_view(unittest.TestCase):
SCRIPT_NAME='/script_name')
view(context, request)
self.assertEqual(request.copied, True)
- self.assertEqual(request.environ['PATH_INFO'], '/path_info')
- self.assertEqual(request.environ['SCRIPT_NAME'], '/script_name')
+ self.assertEqual(request.environ['PATH_INFO'], '/')
+ self.assertEqual(request.environ['SCRIPT_NAME'],
+ '/script_name/path_info')
def test_with_subpath_path_info_ends_with_slash(self):
view = self._makeOne('fixtures', package_name='another')
@@ -295,7 +296,7 @@ class Test_static_view(unittest.TestCase):
self.assertEqual(request.environ['SCRIPT_NAME'],
'/scriptname/path_info')
- def test_with_subpath_new_script_name_fixes_trailing_double_slashes(self):
+ def test_with_subpath_new_script_name_fixes_trailing_slashes(self):
view = self._makeOne('fixtures', package_name='another')
context = DummyContext()
request = DummyRequest()
@@ -304,7 +305,7 @@ class Test_static_view(unittest.TestCase):
view(context, request)
self.assertEqual(request.copied, True)
self.assertEqual(request.environ['PATH_INFO'], '/sub/path/')
- self.assertEqual(request.environ['SCRIPT_NAME'], '/path_info/')
+ self.assertEqual(request.environ['SCRIPT_NAME'], '/path_info')
class TestStaticURLInfo(unittest.TestCase):
def _getTargetClass(self):
@@ -390,43 +391,83 @@ class TestStaticURLInfo(unittest.TestCase):
def test_add_viewname(self):
from pyramid.static import static_view
- class Config:
- def add_route(self, *arg, **kw):
- self.arg = arg
- self.kw = kw
- config = Config()
+ config = DummyConfig()
inst = self._makeOne(config)
inst.add('view', 'anotherpackage:path', cache_max_age=1)
expected = [('view/', 'anotherpackage:path/', False)]
self.assertEqual(inst.registrations, expected)
- self.assertEqual(config.arg, ('view/', 'view/*subpath'))
- self.assertEqual(config.kw['view_permission'],
+ self.assertEqual(config.route_args, ('view/', 'view/*subpath'))
+ self.assertEqual(config.view_kw['permission'],
'__no_permission_required__')
- self.assertEqual(config.kw['view'].__class__, static_view)
- self.assertEqual(config.kw['view'].app.cache_max_age, 1)
- self.assertEqual(inst.registrations, expected)
+ self.assertEqual(config.view_kw['view'].__class__, static_view)
+ self.assertEqual(config.view_kw['view'].app.cache_max_age, 1)
def test_add_viewname_with_permission(self):
- class Config:
- def add_route(self, *arg, **kw):
- self.arg = arg
- self.kw = kw
- config = Config()
+ config = DummyConfig()
inst = self._makeOne(config)
inst.add('view', 'anotherpackage:path', cache_max_age=1,
permission='abc')
- self.assertEqual(config.kw['view_permission'], 'abc')
+ self.assertEqual(config.view_kw['permission'], 'abc')
def test_add_viewname_with_view_permission(self):
- class Config:
- def add_route(self, *arg, **kw):
- self.arg = arg
- self.kw = kw
- config = Config()
+ config = DummyConfig()
inst = self._makeOne(config)
inst.add('view', 'anotherpackage:path', cache_max_age=1,
view_permission='abc')
- self.assertEqual(config.kw['view_permission'], 'abc')
+ self.assertEqual(config.view_kw['permission'], 'abc')
+
+ def test_add_viewname_with_view_context(self):
+ config = DummyConfig()
+ inst = self._makeOne(config)
+ inst.add('view', 'anotherpackage:path', cache_max_age=1,
+ view_context=DummyContext)
+ self.assertEqual(config.view_kw['context'], DummyContext)
+
+ def test_add_viewname_with_view_for(self):
+ config = DummyConfig()
+ inst = self._makeOne(config)
+ inst.add('view', 'anotherpackage:path', cache_max_age=1,
+ view_for=DummyContext)
+ self.assertEqual(config.view_kw['context'], DummyContext)
+
+ def test_add_viewname_with_for_(self):
+ config = DummyConfig()
+ inst = self._makeOne(config)
+ inst.add('view', 'anotherpackage:path', cache_max_age=1,
+ for_=DummyContext)
+ self.assertEqual(config.view_kw['context'], DummyContext)
+
+ def test_add_viewname_with_view_renderer(self):
+ config = DummyConfig()
+ inst = self._makeOne(config)
+ inst.add('view', 'anotherpackage:path', cache_max_age=1,
+ view_renderer='mypackage:templates/index.pt')
+ self.assertEqual(config.view_kw['renderer'],
+ 'mypackage:templates/index.pt')
+
+ def test_add_viewname_with_renderer(self):
+ config = DummyConfig()
+ inst = self._makeOne(config)
+ inst.add('view', 'anotherpackage:path', cache_max_age=1,
+ renderer='mypackage:templates/index.pt')
+ self.assertEqual(config.view_kw['renderer'],
+ 'mypackage:templates/index.pt')
+
+ def test_add_viewname_with_view_attr(self):
+ config = DummyConfig()
+ inst = self._makeOne(config)
+ inst.add('view', 'anotherpackage:path', cache_max_age=1,
+ view_attr='attr')
+ self.assertEqual(config.view_kw['attr'], 'attr')
+
+class DummyConfig:
+ def add_route(self, *args, **kw):
+ self.route_args = args
+ self.route_kw = kw
+
+ def add_view(self, *args, **kw):
+ self.view_args = args
+ self.view_kw = kw
class DummyStartResponse:
def __call__(self, status, headerlist, exc_info=None):
diff --git a/pyramid/tests/test_wsgi.py b/pyramid/tests/test_wsgi.py
index f63667352..06bcf1cb2 100644
--- a/pyramid/tests/test_wsgi.py
+++ b/pyramid/tests/test_wsgi.py
@@ -20,11 +20,9 @@ class WSGIApp2Tests(unittest.TestCase):
def test_decorator_with_subpath_and_view_name(self):
context = DummyContext()
request = DummyRequest()
- request.traversed = ['a', 'b']
- request.virtual_root_path = ['a']
- request.subpath = ['subpath']
- request.view_name = 'view_name'
- request.environ = {'SCRIPT_NAME':'/foo'}
+ request.subpath = ('subpath',)
+ request.environ = {'SCRIPT_NAME':'/foo',
+ 'PATH_INFO':'/b/view_name/subpath'}
decorator = self._callFUT(dummyapp)
response = decorator(context, request)
self.assertEqual(response, dummyapp)
@@ -34,11 +32,8 @@ class WSGIApp2Tests(unittest.TestCase):
def test_decorator_with_subpath_no_view_name(self):
context = DummyContext()
request = DummyRequest()
- request.traversed = ['a', 'b']
- request.virtual_root_path = ['a']
- request.subpath = ['subpath']
- request.view_name = ''
- request.environ = {'SCRIPT_NAME':'/foo'}
+ request.subpath = ('subpath',)
+ request.environ = {'SCRIPT_NAME':'/foo', 'PATH_INFO':'/b/subpath'}
decorator = self._callFUT(dummyapp)
response = decorator(context, request)
self.assertEqual(response, dummyapp)
@@ -48,11 +43,8 @@ class WSGIApp2Tests(unittest.TestCase):
def test_decorator_no_subpath_with_view_name(self):
context = DummyContext()
request = DummyRequest()
- request.traversed = ['a', 'b']
- request.virtual_root_path = ['a']
- request.subpath = []
- request.view_name = 'view_name'
- request.environ = {'SCRIPT_NAME':'/foo'}
+ request.subpath = ()
+ request.environ = {'SCRIPT_NAME':'/foo', 'PATH_INFO':'/b/view_name'}
decorator = self._callFUT(dummyapp)
response = decorator(context, request)
self.assertEqual(response, dummyapp)
@@ -62,11 +54,8 @@ class WSGIApp2Tests(unittest.TestCase):
def test_decorator_traversed_empty_with_view_name(self):
context = DummyContext()
request = DummyRequest()
- request.traversed = []
- request.virtual_root_path = []
- request.subpath = []
- request.view_name = 'view_name'
- request.environ = {'SCRIPT_NAME':'/foo'}
+ request.subpath = ()
+ request.environ = {'SCRIPT_NAME':'/foo', 'PATH_INFO':'/view_name'}
decorator = self._callFUT(dummyapp)
response = decorator(context, request)
self.assertEqual(response, dummyapp)
@@ -76,11 +65,8 @@ class WSGIApp2Tests(unittest.TestCase):
def test_decorator_traversed_empty_no_view_name(self):
context = DummyContext()
request = DummyRequest()
- request.traversed = []
- request.virtual_root_path = []
- request.subpath = []
- request.view_name = ''
- request.environ = {'SCRIPT_NAME':'/foo'}
+ request.subpath = ()
+ request.environ = {'SCRIPT_NAME':'/foo', 'PATH_INFO':'/'}
decorator = self._callFUT(dummyapp)
response = decorator(context, request)
self.assertEqual(response, dummyapp)
@@ -90,11 +76,8 @@ class WSGIApp2Tests(unittest.TestCase):
def test_decorator_traversed_empty_no_view_name_no_script_name(self):
context = DummyContext()
request = DummyRequest()
- request.traversed = []
- request.virtual_root_path = []
- request.subpath = []
- request.view_name = ''
- request.environ = {'SCRIPT_NAME':''}
+ request.subpath = ()
+ request.environ = {'SCRIPT_NAME':'', 'PATH_INFO':'/'}
decorator = self._callFUT(dummyapp)
response = decorator(context, request)
self.assertEqual(response, dummyapp)
@@ -110,3 +93,8 @@ class DummyContext:
class DummyRequest:
def get_response(self, application):
return application
+
+ def copy(self):
+ self.copied = True
+ return self
+
diff --git a/pyramid/tests/wsgiapp2app/__init__.py b/pyramid/tests/wsgiapp2app/__init__.py
new file mode 100644
index 000000000..0880556ef
--- /dev/null
+++ b/pyramid/tests/wsgiapp2app/__init__.py
@@ -0,0 +1,17 @@
+from pyramid.view import view_config
+from pyramid.wsgi import wsgiapp2
+
+@view_config(name='hello', renderer='string')
+@wsgiapp2
+def hello(environ, start_response):
+ assert environ['PATH_INFO'] == '/'
+ assert environ['SCRIPT_NAME'] == '/hello'
+ response_headers = [('Content-Type', 'text/plain')]
+ start_response('200 OK', response_headers)
+ return ['Hello!']
+
+def main():
+ from pyramid.config import Configurator
+ c = Configurator()
+ c.scan()
+ return c
diff --git a/pyramid/wsgi.py b/pyramid/wsgi.py
index e988a000e..e4c61ff63 100644
--- a/pyramid/wsgi.py
+++ b/pyramid/wsgi.py
@@ -1,5 +1,5 @@
from pyramid.compat import wraps
-from pyramid.traversal import quote_path_segment
+from pyramid.request import call_app_with_subpath_as_path_info
def wsgiapp(wrapped):
""" Decorator to turn a WSGI application into a :app:`Pyramid`
@@ -31,7 +31,7 @@ def wsgiapp(wrapped):
"""
def decorator(context, request):
return request.get_response(wrapped)
- return wraps(wrapped)(decorator) # grokkability
+ return wraps(wrapped)(decorator)
def wsgiapp2(wrapped):
""" Decorator to turn a WSGI application into a :app:`Pyramid`
@@ -56,31 +56,15 @@ def wsgiapp2(wrapped):
config.add_view(hello_world, name='hello_world.txt')
The ``wsgiapp2`` decorator will convert the result of the WSGI
- application to a Response and return it to :app:`Pyramid` as if
- the WSGI app were a :app:`Pyramid` view. The ``SCRIPT_NAME``
- and ``PATH_INFO`` values present in the WSGI environment are fixed
- up before the application is invoked. """
+ application to a Response and return it to :app:`Pyramid` as if the WSGI
+ app were a :app:`Pyramid` view. The ``SCRIPT_NAME`` and ``PATH_INFO``
+ values present in the WSGI environment are fixed up before the
+ application is invoked. In particular, a new WSGI environment is
+ generated, and the :term:`subpath` of the request passed to ``wsgiapp2``
+ is used as the new request's ``PATH_INFO`` and everything preceding the
+ subpath is used as the ``SCRIPT_NAME``. The new environment is passed to
+ the downstream WSGI application."""
def decorator(context, request):
- traversed = request.traversed
- vroot_path = request.virtual_root_path
- if not vroot_path:
- vroot_path = ()
- view_name = request.view_name
- subpath = request.subpath
- if not subpath:
- subpath = ()
- script_tuple = traversed[len(vroot_path):]
- script_list = [ quote_path_segment(name) for name in script_tuple ]
- if view_name:
- script_list.append(quote_path_segment(view_name))
- script_name = '/' + '/'.join(script_list)
- path_list = [ quote_path_segment(name) for name in subpath ]
- path_info = '/' + '/'.join(path_list)
- request.environ['PATH_INFO'] = path_info
- script_name = request.environ['SCRIPT_NAME'] + script_name
- if script_name.endswith('/'):
- script_name = script_name[:-1]
- request.environ['SCRIPT_NAME'] = script_name
- return request.get_response(wrapped)
- return wraps(wrapped)(decorator) # grokkability
+ return call_app_with_subpath_as_path_info(request, wrapped)
+ return wraps(wrapped)(decorator)
diff --git a/setup.py b/setup.py
index ac9db9ab7..5208f58a1 100644
--- a/setup.py
+++ b/setup.py
@@ -53,7 +53,7 @@ if sys.version_info[:2] < (2, 6):
install_requires.append('simplejson')
setup(name='pyramid',
- version='1.0',
+ version='1.1a0',
description=('The Pyramid web application development framework, a '
'Pylons project'),
long_description=README + '\n\n' + CHANGES,