diff options
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 ------------ @@ -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) @@ -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, |
