summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/glossary.rst6
-rw-r--r--docs/narr/extconfig.rst1
-rw-r--r--docs/narr/viewconfig.rst107
-rw-r--r--pyramid/config/__init__.py1
-rw-r--r--pyramid/config/routes.py37
-rw-r--r--pyramid/config/views.py151
-rw-r--r--pyramid/interfaces.py7
-rw-r--r--pyramid/predicates.py6
-rw-r--r--pyramid/testing.py2
9 files changed, 244 insertions, 74 deletions
diff --git a/docs/glossary.rst b/docs/glossary.rst
index b05344ae9..7244aeb8d 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -1209,3 +1209,9 @@ Glossary
Alembic
`Alembic <http://alembic.zzzcomputing.com/en/latest/>`_ is a lightweight database migration tool for usage with the SQLAlchemy Database Toolkit for Python.
+
+ media type
+ A label representing the type of some content.
+ A media type is a nested structure containing a top-level type and a subtype.
+ Optionally, a media type can also contain parameters specific to the type.
+ See :rfc:`6838` for more information about media types.
diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst
index 61bd7a05f..a9ea93e60 100644
--- a/docs/narr/extconfig.rst
+++ b/docs/narr/extconfig.rst
@@ -255,6 +255,7 @@ Pre-defined Phases
:const:`pyramid.config.PHASE1_CONFIG`
+- :meth:`pyramid.config.Configurator.add_accept_view_option`
- :meth:`pyramid.config.Configurator.add_renderer`
- :meth:`pyramid.config.Configurator.add_route_predicate`
- :meth:`pyramid.config.Configurator.add_subscriber_predicate`
diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst
index c463d297e..e85338573 100644
--- a/docs/narr/viewconfig.rst
+++ b/docs/narr/viewconfig.rst
@@ -285,6 +285,17 @@ Non-Predicate Arguments
are just developing stock Pyramid applications. Pay no attention to the man
behind the curtain.
+``exception_only``
+
+ When this value is ``True``, the ``context`` argument must be a subclass of
+ ``Exception``. This flag indicates that only an :term:`exception view` should
+ be created, and that this view should not match if the traversal
+ :term:`context` matches the ``context`` argument. If the ``context`` is a
+ subclass of ``Exception`` and this value is ``False`` (the default), then a
+ view will be registered to match the traversal :term:`context` as well.
+
+ .. versionadded:: 1.8
+
Predicate Arguments
+++++++++++++++++++
@@ -317,17 +328,6 @@ configured view.
If ``context`` is not supplied, the value ``None``, which matches any
resource, is used.
-``exception_only``
-
- When this value is ``True``, the ``context`` argument must be a subclass of
- ``Exception``. This flag indicates that only an :term:`exception view` should
- be created, and that this view should not match if the traversal
- :term:`context` matches the ``context`` argument. If the ``context`` is a
- subclass of ``Exception`` and this value is ``False`` (the default), then a
- view will be registered to match the traversal :term:`context` as well.
-
- .. versionadded:: 1.8
-
``route_name``
If ``route_name`` is supplied, the view callable will be invoked only when
the named route has matched.
@@ -344,6 +344,20 @@ configured view.
request/context pair found via :term:`resource location` does not indicate it
matched any configured route.
+``accept``
+ A :term:`media type` that will be matched against the ``Accept`` HTTP request header.
+ If this value is specified, it must be a specific media type, such as ``text/html``.
+ If the media type is acceptable by the ``Accept`` header of the request, or if the ``Accept`` header isn't set at all in the request, this predicate will match.
+ If this does not match the ``Accept`` header of the request, view matching continues.
+
+ If ``accept`` is not specified, the ``HTTP_ACCEPT`` HTTP header is not taken into consideration when deciding whether or not to invoke the associated view callable.
+
+ See :ref:`accept_content_negotation` for more information.
+
+ .. versionchanged:: 1.10
+ Media ranges such as ``text/*`` will now raise :class:`pyramid.exceptions.ConfigurationError`.
+ Previously these values had undefined behavior based on the version of WebOb being used and was never fully supported.
+
``request_type``
This value should be an :term:`interface` that the :term:`request` must
provide in order for this view to be found and called.
@@ -424,19 +438,6 @@ configured view.
taken into consideration when deciding whether or not to invoke the
associated view callable.
-``accept``
- The value of this argument represents a match query for one or more mimetypes
- in the ``Accept`` HTTP request header. If this value is specified, it must
- be in one of the following forms: a mimetype match token in the form
- ``text/plain``, a wildcard mimetype match token in the form ``text/*``, or a
- match-all wildcard mimetype match token in the form ``*/*``. If any of the
- forms matches the ``Accept`` header of the request, this predicate will be
- true.
-
- If ``accept`` is not specified, the ``HTTP_ACCEPT`` HTTP header is not taken
- into consideration when deciding whether or not to invoke the associated view
- callable.
-
``header``
This value represents an HTTP header name or a header name/value pair.
@@ -1028,6 +1029,64 @@ these values.
.. index::
single: HTTP caching
+.. _accept_content_negotation:
+
+Accept Header Content Negotiation
+---------------------------------
+
+The ``accept`` argument to :meth:`pyramid.config.Configurator.add_view` can be used to control :term:`view lookup` by dispatching to different views based on the HTTP ``Accept`` request header.
+Consider the below example in which there are two views, sharing the same view callable.
+Each view specifies uses the accept header to trigger the appropriate response renderer.
+
+.. code-block:: python
+
+ from pyramid.view import view_config
+
+ @view_config(accept='application/json', renderer='json')
+ @view_config(accept='text/html', renderer='templates/hello.jinja2')
+ def myview(request):
+ return {
+ 'name': request.GET.get('name', 'bob'),
+ }
+
+Wildcard Accept header
+++++++++++++++++++++++
+
+The appropriate view is selected here when the client specifies an unambiguous header such as ``Accept: text/*`` or ``Accept: application/json``.
+However, by default, if a client specifies ``Accept: */*``, the ordering is undefined.
+This can be fixed by telling :app:`Pyramid` what the preferred relative ordering is between various accept mimetypes by using :meth:`pyramid.config.Configurator.add_accept_view_option`.
+For example:
+
+.. code-block:: python
+
+ from pyramid.config import Configurator
+
+ def main(global_config, **settings):
+ config = Configurator(settings=settings)
+ config.add_accept_view_option('text/html')
+ config.add_accept_view_option(
+ 'application/json',
+ weighs_more_than='text/html',
+ )
+ config.scan()
+ return config.make_wsgi_app()
+
+Missing Accept header
++++++++++++++++++++++
+
+The above example will not match any view if the ``Accept`` header is not specified by the client.
+This can be solved by adding a fallback view without an ``accept`` predicate.
+For example, below the html response will be returned in all cases unless ``application/json`` is requested specifically.
+
+.. code-block:: python
+
+ @view_config(accept='application/json', renderer='json')
+ @view_config(renderer='templates/hello.jinja2')
+ def myview(request):
+ return {
+ 'name': request.GET.get('name', 'bob'),
+ }
+
.. _influencing_http_caching:
Influencing HTTP Caching
diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py
index f4fcf413e..2f4e133f0 100644
--- a/pyramid/config/__init__.py
+++ b/pyramid/config/__init__.py
@@ -394,6 +394,7 @@ class Configurator(
self.add_default_response_adapters()
self.add_default_renderers()
+ self.add_default_accept_view_order()
self.add_default_view_predicates()
self.add_default_view_derivers()
self.add_default_route_predicates()
diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py
index 904c7bd4e..051bd9edb 100644
--- a/pyramid/config/routes.py
+++ b/pyramid/config/routes.py
@@ -139,18 +139,6 @@ class RoutesConfiguratorMixin(object):
.. versionadded:: 1.1
- accept
-
- This value represents a match query for one or more mimetypes in the
- ``Accept`` HTTP request header. If this value is specified, it must
- be in one of the following forms: a mimetype match token in the form
- ``text/plain``, a wildcard mimetype match token in the form
- ``text/*`` or a match-all wildcard mimetype match token in the form
- ``*/*``. If any of the forms matches the ``Accept`` header of the
- request, or if the ``Accept`` header isn't set at all in the request,
- this will match the current route. If this does not match the
- ``Accept`` header of the request, route matching continues.
-
Predicate Arguments
pattern
@@ -233,6 +221,27 @@ class RoutesConfiguratorMixin(object):
case of the header name is not significant. If this
predicate returns ``False``, route matching continues.
+ accept
+
+ A media type that will be matched against the ``Accept`` HTTP
+ request header. If this value is specified, it must be a specific
+ media type, such as ``text/html``. If the media type is acceptable
+ by the ``Accept`` header of the request, or if the ``Accept`` header
+ isn't set at all in the request, this predicate will match. If this
+ does not match the ``Accept`` header of the request, route matching
+ continues.
+
+ If ``accept`` is not specified, the ``HTTP_ACCEPT`` HTTP header is
+ not taken into consideration when deciding whether or not to select
+ the route.
+
+
+ .. versionchanged:: 1.10
+ Media ranges such as ``text/*`` will now raise
+ :class:`pyramid.exceptions.ConfigurationError`. Previously,
+ these values had undefined behavior based on the version of
+ WebOb being used and was never fully supported.
+
effective_principals
If specified, this value should be a :term:`principal` identifier or
@@ -289,6 +298,10 @@ class RoutesConfiguratorMixin(object):
DeprecationWarning,
stacklevel=3
)
+
+ if accept is not None:
+ accept = accept.lower()
+
# these are route predicates; if they do not match, the next route
# in the routelist will be tried
if request_method is not None:
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index 5d46de276..4eab27542 100644
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -13,6 +13,7 @@ from zope.interface import (
from zope.interface.interfaces import IInterface
from pyramid.interfaces import (
+ IAcceptOrder,
IExceptionViewClassifier,
IException,
IMultiView,
@@ -115,14 +116,14 @@ class MultiView(object):
view = self.match(context, request)
return view.__discriminator__(context, request)
- def add(self, view, order, accept=None, phash=None):
+ def add(self, view, order, accept=None, phash=None, accept_order=None):
if phash is not None:
for i, (s, v, h) in enumerate(list(self.views)):
if phash == h:
self.views[i] = (order, view, phash)
return
- if accept is None or '*' in accept:
+ if accept is None:
self.views.append((order, view, phash))
self.views.sort(key=operator.itemgetter(0))
else:
@@ -134,21 +135,24 @@ class MultiView(object):
else:
subset.append((order, view, phash))
subset.sort(key=operator.itemgetter(0))
+ # dedupe accepts and sort appropriately
accepts = set(self.accepts)
accepts.add(accept)
- self.accepts = list(accepts) # dedupe
+ if accept_order is not None:
+ sorted_accepts = []
+ for accept in accept_order.sorted():
+ if accept in accepts:
+ sorted_accepts.append(accept)
+ accepts.remove(accept)
+ sorted_accepts.extend(accepts)
+ accepts = sorted_accepts
+ self.accepts = list(accepts)
def get_views(self, request):
if self.accepts and hasattr(request, 'accept'):
- accepts = self.accepts[:]
views = []
- while accepts:
- match = request.accept.best_match(accepts)
- if match is None:
- break
- subset = self.media_views[match]
- views.extend(subset)
- accepts.remove(match)
+ for offer, _ in request.accept.acceptable_offers(self.accepts):
+ views.extend(self.media_views[offer])
views.extend(self.views)
return views
return self.views
@@ -533,17 +537,17 @@ class ViewsConfiguratorMixin(object):
very useful for 'civilians' who are just developing stock Pyramid
applications. Pay no attention to the man behind the curtain.
- accept
+ exception_only
- This value represents a match query for one or more mimetypes in the
- ``Accept`` HTTP request header. If this value is specified, it must
- be in one of the following forms: a mimetype match token in the form
- ``text/plain``, a wildcard mimetype match token in the form
- ``text/*`` or a match-all wildcard mimetype match token in the form
- ``*/*``. If any of the forms matches the ``Accept`` header of the
- request, or if the ``Accept`` header isn't set at all in the request,
- this will match the current view. If this does not match the
- ``Accept`` header of the request, view matching continues.
+ .. versionadded:: 1.8
+
+ When this value is ``True``, the ``context`` argument must be
+ a subclass of ``Exception``. This flag indicates that only an
+ :term:`exception view` should be created, and that this view should
+ not match if the traversal :term:`context` matches the ``context``
+ argument. If the ``context`` is a subclass of ``Exception`` and
+ this value is ``False`` (the default), then a view will be
+ registered to match the traversal :term:`context` as well.
Predicate Arguments
@@ -566,18 +570,6 @@ class ViewsConfiguratorMixin(object):
spelling). If the view should *only* match when handling
exceptions, then set the ``exception_only`` to ``True``.
- exception_only
-
- .. versionadded:: 1.8
-
- When this value is ``True``, the ``context`` argument must be
- a subclass of ``Exception``. This flag indicates that only an
- :term:`exception view` should be created, and that this view should
- not match if the traversal :term:`context` matches the ``context``
- argument. If the ``context`` is a subclass of ``Exception`` and
- this value is ``False`` (the default), then a view will be
- registered to match the traversal :term:`context` as well.
-
route_name
This value must match the ``name`` of a :term:`route
@@ -677,6 +669,28 @@ class ViewsConfiguratorMixin(object):
represents a header name or a header name/value pair, the
case of the header name is not significant.
+ accept
+
+ A :term:`media type` that will be matched against the ``Accept``
+ HTTP request header. If this value is specified, it must be a
+ specific media type, such as ``text/html``. If the media type is
+ acceptable by the ``Accept`` header of the request, or if the
+ ``Accept`` header isn't set at all in the request, this predicate
+ will match. If this does not match the ``Accept`` header of the
+ request, view matching continues.
+
+ If ``accept`` is not specified, the ``HTTP_ACCEPT`` HTTP header is
+ not taken into consideration when deciding whether or not to invoke
+ the associated view callable.
+
+ See :ref:`accept_content_negotation` for more information.
+
+ .. versionchanged:: 1.10
+ Media ranges such as ``text/*`` will now raise
+ :class:`pyramid.exceptions.ConfigurationError`. Previously,
+ these values had undefined behavior based on the version of
+ WebOb being used and was never fully supported.
+
path_info
This value represents a regular expression pattern that will
@@ -804,6 +818,9 @@ class ViewsConfiguratorMixin(object):
stacklevel=4,
)
+ if accept is not None:
+ accept = accept.lower()
+
view = self.maybe_dotted(view)
context = self.maybe_dotted(context)
for_ = self.maybe_dotted(for_)
@@ -857,9 +874,6 @@ class ViewsConfiguratorMixin(object):
name=renderer, package=self.package,
registry=self.registry)
- if accept is not None:
- accept = accept.lower()
-
introspectables = []
ovals = view_options.copy()
ovals.update(dict(
@@ -1104,8 +1118,11 @@ class ViewsConfiguratorMixin(object):
multiview = MultiView(name)
old_accept = getattr(old_view, '__accept__', None)
old_order = getattr(old_view, '__order__', MAX_ORDER)
+ # don't bother passing accept_order here as we know we're
+ # adding another one right after which will re-sort
multiview.add(old_view, old_order, old_accept, old_phash)
- multiview.add(derived_view, order, accept, phash)
+ accept_order = self.registry.queryUtility(IAcceptOrder)
+ multiview.add(derived_view, order, accept, phash, accept_order)
for view_type in (IView, ISecuredView):
# unregister any existing views
self.registry.adapters.unregister(
@@ -1222,6 +1239,66 @@ class ViewsConfiguratorMixin(object):
):
self.add_view_predicate(name, factory)
+ def add_default_accept_view_order(self):
+ for accept in (
+ 'text/html',
+ 'application/xhtml+xml',
+ 'application/xml',
+ 'text/xml',
+ 'application/json',
+ ):
+ self.add_accept_view_order(accept)
+
+ @action_method
+ def add_accept_view_order(
+ self,
+ value,
+ weighs_more_than=None,
+ weighs_less_than=None,
+ ):
+ """
+ Specify an ordering preference for the ``accept`` view option used
+ during :term:`view lookup`.
+
+ By default, if two views have different ``accept`` options and a
+ request specifies ``Accept: */*`` or omits the header entirely then
+ it is random which view will be selected. This method provides a way
+ to specify a server-side, relative ordering between accept media types.
+
+ ``value`` should be a :term:`media type` as specified by
+ :rfc:`7231#section-5.3.2`. For example, ``text/plain``,
+ ``application/json`` or ``text/html``.
+
+ ``weighs_more_than`` and ``weighs_less_than`` control the ordering
+ of media types. Each value may be a string or a list of strings.
+
+ See :ref:`accept_content_negotation` for more information.
+
+ .. versionadded:: 1.10
+
+ """
+ discriminator = ('accept view order', value)
+ intr = self.introspectable(
+ 'accept view order',
+ value,
+ value,
+ 'accept view order')
+ intr['value'] = value
+ intr['weighs_more_than'] = weighs_more_than
+ intr['weighs_less_than'] = weighs_less_than
+ def register():
+ sorter = self.registry.queryUtility(IAcceptOrder)
+ if sorter is None:
+ sorter = TopologicalSorter()
+ self.registry.registerUtility(sorter, IAcceptOrder)
+ sorter.add(
+ value, value,
+ after=weighs_more_than,
+ before=weighs_less_than,
+ )
+ self.action(discriminator, register, introspectables=(intr,),
+ order=PHASE1_CONFIG) # must be registered before add_view
+
@action_method
def add_view_deriver(self, deriver, name=None, under=None, over=None):
"""
diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py
index bedfb60b3..17673087d 100644
--- a/pyramid/interfaces.py
+++ b/pyramid/interfaces.py
@@ -586,6 +586,13 @@ class IRouteRequest(Interface):
""" *internal only* interface used as in a utility lookup to find
route-specific interfaces. Not an API."""
+class IAcceptOrder(Interface):
+ """
+ Marker interface for a list of accept headers with the most important
+ first.
+
+ """
+
class IStaticURLInfo(Interface):
""" A policy for generating URLs to static assets """
def add(config, name, spec, **extra):
diff --git a/pyramid/predicates.py b/pyramid/predicates.py
index 5e54badff..4f63122aa 100644
--- a/pyramid/predicates.py
+++ b/pyramid/predicates.py
@@ -132,6 +132,10 @@ class HeaderPredicate(object):
class AcceptPredicate(object):
def __init__(self, val, config):
self.val = val
+ if '*' in self.val:
+ raise ConfigurationError(
+ '"accept" predicate only accepts specific media types',
+ )
def text(self):
return 'accept = %s' % (self.val,)
@@ -139,7 +143,7 @@ class AcceptPredicate(object):
phash = text
def __call__(self, context, request):
- return self.val in request.accept
+ return bool(request.accept.acceptable_offers([self.val]))
class ContainmentPredicate(object):
def __init__(self, val, config):
diff --git a/pyramid/testing.py b/pyramid/testing.py
index 7ff4c2f73..4986c0e27 100644
--- a/pyramid/testing.py
+++ b/pyramid/testing.py
@@ -474,7 +474,9 @@ def setUp(registry=None, request=None, hook_zca=True, autocommit=True,
# someone may be passing us an esoteric "dummy" registry, and
# the below won't succeed if it doesn't have a registerUtility
# method.
+ config.add_default_response_adapters()
config.add_default_renderers()
+ config.add_default_accept_view_order()
config.add_default_view_predicates()
config.add_default_view_derivers()
config.add_default_route_predicates()