Merge pull request #3326 from mmerickel/fix-deprecated-accept-predicate
Overhaul HTTP Accept (mime type) handling in Pyramid
| | |
| | | - Add support for Python 3.7. Add testing on Python 3.8 with allowed failures. |
| | | See https://github.com/Pylons/pyramid/pull/3333 |
| | | |
| | | - Added the ``pyramid.config.Configurator.add_accept_view_order`` directive, |
| | | allowing users to specify media type preferences in ambiguous situations |
| | | such as when several views match. A default ordering is defined for media |
| | | types that prefers human-readable html/text responses over JSON. |
| | | See https://github.com/Pylons/pyramid/pull/3326 |
| | | |
| | | - Support a list of media types in the ``accept`` predicate used in |
| | | ``pyramid.config.Configurator.add_route``. |
| | | See https://github.com/Pylons/pyramid/pull/3326 |
| | | |
| | | - Added ``pyramid.session.JSONSerializer``. See "Upcoming Changes to ISession |
| | | in Pyramid 2.0" in the "Sessions" chapter of the documentation for more |
| | | information about this feature. |
| | |
| | | implementation if you're still using these features. |
| | | See https://github.com/Pylons/pyramid/pull/3353 |
| | | |
| | | - Media ranges are deprecated in the ``accept`` argument of |
| | | ``pyramid.config.Configurator.add_route``. Use a list of explicit |
| | | media types to ``add_route`` to support multiple types. |
| | | |
| | | - Media ranges are deprecated in the ``accept`` argument of |
| | | ``pyramid.config.Configurator.add_view``. There is no replacement for |
| | | ranges to ``add_view``, but after much discussion the workflow is |
| | | fundamentally ambiguous in the face of various client-supplied values for |
| | | the ``Accept`` header. |
| | | See https://github.com/Pylons/pyramid/pull/3326 |
| | | |
| | | Backward Incompatibilities |
| | | -------------------------- |
| | | |
| | |
| | | |
| | | 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. |
| | |
| | | |
| | | :const:`pyramid.config.PHASE1_CONFIG` |
| | | |
| | | - :meth:`pyramid.config.Configurator.add_accept_view_order` |
| | | - :meth:`pyramid.config.Configurator.add_renderer` |
| | | - :meth:`pyramid.config.Configurator.add_route_predicate` |
| | | - :meth:`pyramid.config.Configurator.add_subscriber_predicate` |
| | | - :meth:`pyramid.config.Configurator.add_view_predicate` |
| | | - :meth:`pyramid.config.Configurator.add_view_deriver` |
| | | - :meth:`pyramid.config.Configurator.add_view_predicate` |
| | | - :meth:`pyramid.config.Configurator.override_asset` |
| | | - :meth:`pyramid.config.Configurator.set_authorization_policy` |
| | | - :meth:`pyramid.config.Configurator.set_default_csrf_options` |
| | |
| | | are just developing stock Pyramid applications. Pay no attention to the man |
| | | behind the curtain. |
| | | |
| | | ``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`` or ``text/html;level=1``. |
| | | 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. |
| | | |
| | | The ``accept`` argument is technically not a predicate and does not support wrapping with :func:`pyramid.config.not_`. |
| | | |
| | | See :ref:`accept_content_negotiation` for more information. |
| | | |
| | | .. versionchanged:: 1.10 |
| | | |
| | | Specifying a media range is deprecated and will be removed in :app:`Pyramid` 2.0. |
| | | Use explicit media types to avoid any ambiguities in content negotiation. |
| | | |
| | | ``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 |
| | | +++++++++++++++++++ |
| | | |
| | |
| | | |
| | | 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 |
| | |
| | | If ``xhr`` is not specified, the ``HTTP_X_REQUESTED_WITH`` HTTP header is not |
| | | 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. |
| | |
| | | these values. |
| | | |
| | | .. index:: |
| | | single: Accept |
| | | single: Accept content negotiation |
| | | |
| | | .. _accept_content_negotiation: |
| | | |
| | | 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 example below in which there are three views configured. |
| | | |
| | | .. code-block:: python |
| | | |
| | | from pyramid.httpexceptions import HTTPNotAcceptable |
| | | 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'), |
| | | } |
| | | |
| | | @view_config() |
| | | def myview_unacceptable(request): |
| | | raise HTTPNotAcceptable |
| | | |
| | | Each view relies on the ``Accept`` header to trigger an appropriate response renderer. |
| | | The appropriate view is selected here when the client specifies headers such as ``Accept: text/*`` or ``Accept: application/json, text/html;q=0.9`` in which only one of the views matches or it's clear based on the preferences which one should win. |
| | | Similarly, if the client specifies a media type that no view is registered to handle, such as ``Accept: text/plain``, it will fall through to ``myview_unacceptable`` and raise ``406 Not Acceptable``. |
| | | |
| | | There are a few cases in which the client may specify an ``Accept`` header such that it's not clear which view should win. |
| | | For example: |
| | | |
| | | - ``Accept: */*``. |
| | | - More than one acceptable media type with the same quality. |
| | | - A missing ``Accept`` header. |
| | | - An invalid ``Accept`` header. |
| | | |
| | | In these cases the preferred view is not clearly defined (see :rfc:`7231#section-5.3.2`) and :app:`Pyramid` will select one randomly. |
| | | This can be controlled by telling :app:`Pyramid` what the preferred relative ordering is between various media types by using :meth:`pyramid.config.Configurator.add_accept_view_order`. |
| | | For example: |
| | | |
| | | .. code-block:: python |
| | | |
| | | from pyramid.config import Configurator |
| | | |
| | | def main(global_config, **settings): |
| | | config = Configurator(settings=settings) |
| | | config.add_accept_view_order('text/html') |
| | | config.add_accept_view_order( |
| | | 'application/json', |
| | | weighs_more_than='text/html', |
| | | ) |
| | | config.scan() |
| | | return config.make_wsgi_app() |
| | | |
| | | Now, the ``application/json`` view should always be preferred in cases where the client wasn't clear. |
| | | |
| | | .. index:: |
| | | single: default accept ordering |
| | | |
| | | .. _default_accept_ordering: |
| | | |
| | | Default Accept Ordering |
| | | ~~~~~~~~~~~~~~~~~~~~~~~ |
| | | |
| | | :app:`Pyramid` will always sort multiple views with the same ``(name, context, route_name)`` first by the specificity of the ``accept`` offer. |
| | | For any set of media type offers with the same ``type/subtype``, the offers with params will weigh more than the bare ``type/subtype`` offer. |
| | | This means that ``text/plain;charset=utf8`` will always be offered before ``text/plain``. |
| | | |
| | | By default, within a given ``type/subtype``, the order of offers is unspecified. |
| | | For example, ``text/plain;charset=utf8`` versus ``text/plain;charset=latin1`` are sorted randomly. |
| | | Similarly, between media types the order is also unspecified other than the defaults described below. |
| | | For example, ``image/jpeg`` versus ``image/png`` versus ``application/pdf``. |
| | | In these cases, the ordering may be controlled using :meth:`pyramid.config.Configurator.add_accept_view_order`. |
| | | For example, to sort ``text/plain`` higher than ``text/html`` and to prefer a ``charset=utf8`` versus a ``charset=latin-1`` within the ``text/plain`` media type: |
| | | |
| | | .. code-block:: python |
| | | |
| | | config.add_accept_view_order('text/html') |
| | | config.add_accept_view_order('text/plain;charset=latin-1') |
| | | config.add_accept_view_order('text/plain', weighs_more_than='text/html') |
| | | config.add_accept_view_order('text/plain;charset=utf8', weighs_more_than='text/plain;charset=latin-1') |
| | | |
| | | It is an error to try and sort accept headers across levels of specificity. |
| | | You can only sort a ``type/subtype`` against another ``type/subtype``, not against a ``type/subtype;params``. |
| | | That ordering is a hard requirement. |
| | | |
| | | By default, :app:`Pyramid` defines a very simple priority ordering for views that prefers human-readable responses over JSON: |
| | | |
| | | - ``text/html`` |
| | | - ``application/xhtml+xml`` |
| | | - ``application/xml`` |
| | | - ``text/xml`` |
| | | - ``text/plain`` |
| | | - ``application/json`` |
| | | |
| | | API clients tend to be able to specify their desired headers with more control than web browsers, and can specify the correct ``Accept`` value, if necessary. |
| | | Therefore, the motivation for this ordering is to optimize for readability. |
| | | Media types that are not listed above are ordered randomly during :term:`view lookup` between otherwise-similar views. |
| | | The defaults can be overridden using :meth:`pyramid.config.Configurator.add_accept_view_order` as described above. |
| | | |
| | | .. index:: |
| | | single: HTTP caching |
| | | |
| | | .. _influencing_http_caching: |
| | |
| | | |
| | | 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() |
| | |
| | | from pyramid.request import route_request_iface |
| | | from pyramid.urldispatch import RoutesMapper |
| | | |
| | | from pyramid.util import as_sorted_tuple |
| | | from pyramid.util import ( |
| | | as_sorted_tuple, |
| | | is_nonstr_iter, |
| | | ) |
| | | |
| | | import pyramid.predicates |
| | | |
| | | from pyramid.config.util import ( |
| | | action_method, |
| | | normalize_accept_offer, |
| | | predvalseq, |
| | | ) |
| | | |
| | |
| | | |
| | | .. 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 |
| | |
| | | case of the header name is not significant. If this |
| | | predicate returns ``False``, route matching continues. |
| | | |
| | | accept |
| | | |
| | | A :term:`media type` that will be matched against the ``Accept`` |
| | | HTTP request header. If this value is specified, it may be a |
| | | specific media type such as ``text/html``, or a list of the same. |
| | | 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. |
| | | |
| | | Unlike the ``accept`` argument to |
| | | :meth:`pyramid.config.Configurator.add_view`, this value is |
| | | strictly a predicate and supports :func:`pyramid.config.not_`. |
| | | |
| | | .. versionchanged:: 1.10 |
| | | |
| | | Specifying a media range is deprecated due to changes in WebOb |
| | | and ambiguities that occur when trying to match ranges against |
| | | ranges in the ``Accept`` header. Support will be removed in |
| | | :app:`Pyramid` 2.0. Use a list of specific media types to match |
| | | more than one type. |
| | | |
| | | effective_principals |
| | | |
| | | If specified, this value should be a :term:`principal` identifier or |
| | |
| | | DeprecationWarning, |
| | | stacklevel=3 |
| | | ) |
| | | |
| | | if accept is not None: |
| | | if not is_nonstr_iter(accept): |
| | | if '*' in accept: |
| | | warnings.warn( |
| | | ('Passing a media range to the "accept" argument of ' |
| | | 'Configurator.add_route is deprecated as of Pyramid ' |
| | | '1.10. Use a list of explicit media types.'), |
| | | DeprecationWarning, |
| | | stacklevel=3, |
| | | ) |
| | | # XXX switch this to False when range support is dropped |
| | | accept = [normalize_accept_offer(accept, allow_range=True)] |
| | | |
| | | else: |
| | | accept = [ |
| | | normalize_accept_offer(accept_option) |
| | | for accept_option in accept |
| | | ] |
| | | |
| | | # these are route predicates; if they do not match, the next route |
| | | # in the routelist will be tried |
| | | if request_method is not None: |
| | |
| | | import functools |
| | | from hashlib import md5 |
| | | import traceback |
| | | from webob.acceptparse import Accept |
| | | from zope.interface import implementer |
| | | |
| | | from pyramid.compat import ( |
| | |
| | | score = score | bit |
| | | order = (MAX_ORDER - score) / (len(preds) + 1) |
| | | return order, preds, phash.hexdigest() |
| | | |
| | | |
| | | def normalize_accept_offer(offer, allow_range=False): |
| | | if allow_range and '*' in offer: |
| | | return offer.lower() |
| | | return str(Accept.parse_offer(offer)) |
| | | |
| | | |
| | | def sort_accept_offers(offers, order=None): |
| | | """ |
| | | Sort a list of offers by preference. |
| | | |
| | | For a given ``type/subtype`` category of offers, this algorithm will |
| | | always sort offers with params higher than the bare offer. |
| | | |
| | | :param offers: A list of offers to be sorted. |
| | | :param order: A weighted list of offers where items closer to the start of |
| | | the list will be a preferred over items closer to the end. |
| | | :return: A list of offers sorted first by specificity (higher to lower) |
| | | then by ``order``. |
| | | |
| | | """ |
| | | if order is None: |
| | | order = [] |
| | | |
| | | max_weight = len(offers) |
| | | |
| | | def find_order_index(value, default=None): |
| | | return next((i for i, x in enumerate(order) if x == value), default) |
| | | |
| | | def offer_sort_key(value): |
| | | """ |
| | | (type_weight, params_weight) |
| | | |
| | | type_weight: |
| | | - index of specific ``type/subtype`` in order list |
| | | - ``max_weight * 2`` if no match is found |
| | | |
| | | params_weight: |
| | | - index of specific ``type/subtype;params`` in order list |
| | | - ``max_weight`` if not found |
| | | - ``max_weight + 1`` if no params at all |
| | | |
| | | """ |
| | | parsed = Accept.parse_offer(value) |
| | | |
| | | type_w = find_order_index( |
| | | parsed.type + '/' + parsed.subtype, |
| | | max_weight, |
| | | ) |
| | | |
| | | if parsed.params: |
| | | param_w = find_order_index(value, max_weight) |
| | | |
| | | else: |
| | | param_w = max_weight + 1 |
| | | |
| | | return (type_w, param_w) |
| | | |
| | | return sorted(offers, key=offer_sort_key) |
| | |
| | | import os |
| | | import warnings |
| | | |
| | | from webob.acceptparse import Accept |
| | | from zope.interface import ( |
| | | Interface, |
| | | implementedBy, |
| | |
| | | from zope.interface.interfaces import IInterface |
| | | |
| | | from pyramid.interfaces import ( |
| | | IAcceptOrder, |
| | | IExceptionViewClassifier, |
| | | IException, |
| | | IMultiView, |
| | |
| | | action_method, |
| | | DEFAULT_PHASH, |
| | | MAX_ORDER, |
| | | normalize_accept_offer, |
| | | predvalseq, |
| | | sort_accept_offers, |
| | | ) |
| | | |
| | | urljoin = urlparse.urljoin |
| | |
| | | view = self.match(context, request) |
| | | return view.__discriminator__(context, request) |
| | | |
| | | def add(self, view, order, accept=None, phash=None): |
| | | def add(self, view, order, phash=None, accept=None, accept_order=None): |
| | | if phash is not None: |
| | | for i, (s, v, h) in enumerate(list(self.views)): |
| | | if phash == h: |
| | |
| | | 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: |
| | | accept_order = [v for _, v in accept_order.sorted()] |
| | | self.accepts = sort_accept_offers(accepts, accept_order) |
| | | |
| | | 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 |
| | |
| | | defaults.update(kw) |
| | | return wrapped(self, *arg, **defaults) |
| | | return functools.wraps(wrapped)(wrapper) |
| | | |
| | | def combine_decorators(*decorators): |
| | | def decorated(view_callable): |
| | | # reversed() allows a more natural ordering in the api |
| | | for decorator in reversed(decorators): |
| | | view_callable = decorator(view_callable) |
| | | return view_callable |
| | | return decorated |
| | | |
| | | class ViewsConfiguratorMixin(object): |
| | | @viewdefaults |
| | |
| | | |
| | | 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 |
| | | 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`` or ``text/html;level=1``. |
| | | 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 will match the current view. If this does not match the |
| | | ``Accept`` header of the request, view matching continues. |
| | | 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. |
| | | |
| | | The ``accept`` argument is technically not a predicate and does |
| | | not support wrapping with :func:`pyramid.config.not_`. |
| | | |
| | | See :ref:`accept_content_negotiation` for more information. |
| | | |
| | | .. versionchanged:: 1.10 |
| | | |
| | | Specifying a media range is deprecated and will be removed in |
| | | :app:`Pyramid` 2.0. Use explicit media types to avoid any |
| | | ambiguities in content negotiation. |
| | | |
| | | 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. |
| | | |
| | | Predicate Arguments |
| | | |
| | |
| | | to ``add_view`` as ``for_`` (an older, still-supported |
| | | 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 |
| | | |
| | |
| | | stacklevel=4, |
| | | ) |
| | | |
| | | if accept is not None: |
| | | if is_nonstr_iter(accept): |
| | | raise ConfigurationError( |
| | | 'A list is not supported in the "accept" view predicate.', |
| | | ) |
| | | if '*' in accept: |
| | | warnings.warn( |
| | | ('Passing a media range to the "accept" argument of ' |
| | | 'Configurator.add_view is deprecated as of Pyramid 1.10. ' |
| | | 'Use explicit media types to avoid ambiguities in ' |
| | | 'content negotiation that may impact your users.'), |
| | | DeprecationWarning, |
| | | stacklevel=4, |
| | | ) |
| | | # XXX when media ranges are gone, switch allow_range=False |
| | | accept = normalize_accept_offer(accept, allow_range=True) |
| | | |
| | | view = self.maybe_dotted(view) |
| | | context = self.maybe_dotted(context) |
| | | for_ = self.maybe_dotted(for_) |
| | | containment = self.maybe_dotted(containment) |
| | | mapper = self.maybe_dotted(mapper) |
| | | |
| | | def combine(*decorators): |
| | | def decorated(view_callable): |
| | | # reversed() allows a more natural ordering in the api |
| | | for decorator in reversed(decorators): |
| | | view_callable = decorator(view_callable) |
| | | return view_callable |
| | | return decorated |
| | | |
| | | if is_nonstr_iter(decorator): |
| | | decorator = combine(*map(self.maybe_dotted, decorator)) |
| | | decorator = combine_decorators(*map(self.maybe_dotted, decorator)) |
| | | else: |
| | | decorator = self.maybe_dotted(decorator) |
| | | |
| | |
| | | renderer = renderers.RendererHelper( |
| | | name=renderer, package=self.package, |
| | | registry=self.registry) |
| | | |
| | | if accept is not None: |
| | | accept = accept.lower() |
| | | |
| | | introspectables = [] |
| | | ovals = view_options.copy() |
| | |
| | | if old_view is not None: |
| | | break |
| | | |
| | | def regclosure(): |
| | | old_phash = getattr(old_view, '__phash__', DEFAULT_PHASH) |
| | | is_multiview = IMultiView.providedBy(old_view) |
| | | want_multiview = ( |
| | | is_multiview |
| | | # no component was yet registered for exactly this triad |
| | | # or only one was registered but with the same phash, meaning |
| | | # that this view is an override |
| | | or (old_view is not None and old_phash != phash) |
| | | ) |
| | | |
| | | if not want_multiview: |
| | | if hasattr(derived_view, '__call_permissive__'): |
| | | view_iface = ISecuredView |
| | | else: |
| | |
| | | view_iface, |
| | | name |
| | | ) |
| | | |
| | | is_multiview = IMultiView.providedBy(old_view) |
| | | old_phash = getattr(old_view, '__phash__', DEFAULT_PHASH) |
| | | |
| | | if old_view is None: |
| | | # - No component was yet registered for any of our I*View |
| | | # interfaces exactly; this is the first view for this |
| | | # triad. |
| | | regclosure() |
| | | |
| | | elif (not is_multiview) and (old_phash == phash): |
| | | # - A single view component was previously registered with |
| | | # the same predicate hash as this view; this registration |
| | | # is therefore an override. |
| | | regclosure() |
| | | |
| | | else: |
| | | # - A view or multiview was already registered for this |
| | |
| | | multiview = MultiView(name) |
| | | old_accept = getattr(old_view, '__accept__', None) |
| | | old_order = getattr(old_view, '__order__', MAX_ORDER) |
| | | multiview.add(old_view, old_order, old_accept, old_phash) |
| | | multiview.add(derived_view, order, accept, phash) |
| | | # 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_phash, old_accept) |
| | | accept_order = self.registry.queryUtility(IAcceptOrder) |
| | | multiview.add(derived_view, order, phash, accept, accept_order) |
| | | for view_type in (IView, ISecuredView): |
| | | # unregister any existing views |
| | | self.registry.adapters.unregister( |
| | |
| | | ): |
| | | 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', |
| | | 'text/plain', |
| | | '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;charset=utf8``, |
| | | ``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. If |
| | | all options for ``weighs_more_than`` (or ``weighs_less_than``) cannot |
| | | be found, it is an error. |
| | | |
| | | Earlier calls to ``add_accept_view_order`` are given higher priority |
| | | over later calls, assuming similar constraints but standard conflict |
| | | resolution mechanisms can be used to override constraints. |
| | | |
| | | See :ref:`accept_content_negotiation` for more information. |
| | | |
| | | .. versionadded:: 1.10 |
| | | |
| | | """ |
| | | def check_type(than): |
| | | than_type, than_subtype, than_params = Accept.parse_offer(than) |
| | | # text/plain vs text/html;charset=utf8 |
| | | if bool(offer_params) ^ bool(than_params): |
| | | raise ConfigurationError( |
| | | 'cannot compare a media type with params to one without ' |
| | | 'params') |
| | | # text/plain;charset=utf8 vs text/html;charset=utf8 |
| | | if offer_params and ( |
| | | offer_subtype != than_subtype or offer_type != than_type |
| | | ): |
| | | raise ConfigurationError( |
| | | 'cannot compare params across different media types') |
| | | |
| | | def normalize_types(thans): |
| | | thans = [normalize_accept_offer(than) for than in thans] |
| | | for than in thans: |
| | | check_type(than) |
| | | return thans |
| | | |
| | | value = normalize_accept_offer(value) |
| | | offer_type, offer_subtype, offer_params = Accept.parse_offer(value) |
| | | |
| | | if weighs_more_than: |
| | | if not is_nonstr_iter(weighs_more_than): |
| | | weighs_more_than = [weighs_more_than] |
| | | weighs_more_than = normalize_types(weighs_more_than) |
| | | |
| | | if weighs_less_than: |
| | | if not is_nonstr_iter(weighs_less_than): |
| | | weighs_less_than = [weighs_less_than] |
| | | weighs_less_than = normalize_types(weighs_less_than) |
| | | |
| | | 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, |
| | | before=weighs_more_than, |
| | | after=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): |
| | | """ |
| | |
| | | """ *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): |
| | |
| | | return self.val.match(val) is not None |
| | | |
| | | class AcceptPredicate(object): |
| | | def __init__(self, val, config): |
| | | self.val = val |
| | | _is_using_deprecated_ranges = False |
| | | |
| | | def __init__(self, values, config): |
| | | if not is_nonstr_iter(values): |
| | | values = (values,) |
| | | # deprecated media ranges were only supported in versions of the |
| | | # predicate that didn't support lists, so check it here |
| | | if len(values) == 1 and '*' in values[0]: |
| | | self._is_using_deprecated_ranges = True |
| | | self.values = values |
| | | |
| | | def text(self): |
| | | return 'accept = %s' % (self.val,) |
| | | return 'accept = %s' % (', '.join(self.values),) |
| | | |
| | | phash = text |
| | | |
| | | def __call__(self, context, request): |
| | | return self.val in request.accept |
| | | if self._is_using_deprecated_ranges: |
| | | return self.values[0] in request.accept |
| | | return bool(request.accept.acceptable_offers(self.values)) |
| | | |
| | | class ContainmentPredicate(object): |
| | | def __init__(self, val, config): |
| | |
| | | # 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() |
| | |
| | | route = self._assertRoute(config, 'name', 'path', 1) |
| | | predicate = route.predicates[0] |
| | | request = self._makeRequest(config) |
| | | request.accept = ['text/xml'] |
| | | request.accept = DummyAccept('text/xml') |
| | | self.assertEqual(predicate(None, request), True) |
| | | request = self._makeRequest(config) |
| | | request.accept = ['text/html'] |
| | | request.accept = DummyAccept('text/html') |
| | | self.assertEqual(predicate(None, request), False) |
| | | |
| | | def test_add_route_with_accept_list(self): |
| | | config = self._makeOne(autocommit=True) |
| | | config.add_route('name', 'path', accept=['text/xml', 'text/plain']) |
| | | route = self._assertRoute(config, 'name', 'path', 1) |
| | | predicate = route.predicates[0] |
| | | request = self._makeRequest(config) |
| | | request.accept = DummyAccept('text/xml') |
| | | self.assertEqual(predicate(None, request), True) |
| | | request = self._makeRequest(config) |
| | | request.accept = DummyAccept('text/plain') |
| | | self.assertEqual(predicate(None, request), True) |
| | | request = self._makeRequest(config) |
| | | request.accept = DummyAccept('text/html') |
| | | self.assertEqual(predicate(None, request), False) |
| | | |
| | | def test_add_route_with_wildcard_accept(self): |
| | | config = self._makeOne(autocommit=True) |
| | | config.add_route('name', 'path', accept='text/*') |
| | | route = self._assertRoute(config, 'name', 'path', 1) |
| | | predicate = route.predicates[0] |
| | | request = self._makeRequest(config) |
| | | request.accept = DummyAccept('text/xml', contains=True) |
| | | self.assertEqual(predicate(None, request), True) |
| | | request = self._makeRequest(config) |
| | | request.accept = DummyAccept('application/json', contains=False) |
| | | self.assertEqual(predicate(None, request), False) |
| | | |
| | | def test_add_route_no_pattern_with_path(self): |
| | |
| | | self.environ = environ |
| | | self.params = {} |
| | | self.cookies = {} |
| | | |
| | | class DummyAccept(object): |
| | | def __init__(self, *matches, **kw): |
| | | self.matches = list(matches) |
| | | self.contains = kw.pop('contains', False) |
| | | |
| | | def acceptable_offers(self, offers): |
| | | results = [] |
| | | for match in self.matches: |
| | | if match in offers: |
| | | results.append((match, 1.0)) |
| | | return results |
| | | |
| | | def __contains__(self, value): |
| | | return self.contains |
| | |
| | | from pyramid.config.predicates import XHRPredicate |
| | | self.assertEqual(len(w), 1) |
| | | |
| | | class Test_sort_accept_offers(unittest.TestCase): |
| | | def _callFUT(self, offers, order=None): |
| | | from pyramid.config.util import sort_accept_offers |
| | | return sort_accept_offers(offers, order) |
| | | |
| | | def test_default_specificities(self): |
| | | result = self._callFUT(['text/html', 'text/html;charset=utf8']) |
| | | self.assertEqual(result, [ |
| | | 'text/html;charset=utf8', 'text/html', |
| | | ]) |
| | | |
| | | def test_specific_type_order(self): |
| | | result = self._callFUT( |
| | | ['text/html', 'application/json', 'text/html;charset=utf8', 'text/plain'], |
| | | ['application/json', 'text/html'], |
| | | ) |
| | | self.assertEqual(result, [ |
| | | 'application/json', 'text/html;charset=utf8', 'text/html', 'text/plain', |
| | | ]) |
| | | |
| | | def test_params_order(self): |
| | | result = self._callFUT( |
| | | ['text/html;charset=utf8', 'text/html;charset=latin1', 'text/html;foo=bar'], |
| | | ['text/html;charset=latin1', 'text/html;charset=utf8'], |
| | | ) |
| | | self.assertEqual(result, [ |
| | | 'text/html;charset=latin1', 'text/html;charset=utf8', 'text/html;foo=bar', |
| | | ]) |
| | | |
| | | def test_params_inherit_type_prefs(self): |
| | | result = self._callFUT( |
| | | ['text/html;charset=utf8', 'text/plain;charset=latin1'], |
| | | ['text/plain', 'text/html'], |
| | | ) |
| | | self.assertEqual(result, ['text/plain;charset=latin1', 'text/html;charset=utf8']) |
| | | |
| | | class DummyCustomPredicate(object): |
| | | def __init__(self): |
| | | self.__text__ = 'custom predicate' |
| | |
| | | config.add_view(view=view2, renderer=null_renderer) |
| | | wrapper = self._getViewCallable(config) |
| | | self.assertTrue(IMultiView.providedBy(wrapper)) |
| | | self.assertEqual([x[:2] for x in wrapper.views], [(view2, None)]) |
| | | self.assertEqual([(x[0], x[2]) for x in wrapper.views], [(view2, None)]) |
| | | self.assertEqual(wrapper(None, None), 'OK1') |
| | | |
| | | def test_add_view_exc_multiview_replaces_multiviews(self): |
| | |
| | | hot_wrapper = self._getViewCallable( |
| | | config, ctx_iface=implementedBy(RuntimeError)) |
| | | self.assertTrue(IMultiView.providedBy(hot_wrapper)) |
| | | self.assertEqual([x[:2] for x in hot_wrapper.views], [(view2, None)]) |
| | | self.assertEqual([(x[0], x[2]) for x in hot_wrapper.views], [(view2, None)]) |
| | | self.assertEqual(hot_wrapper(None, None), 'OK1') |
| | | |
| | | exc_wrapper = self._getViewCallable( |
| | | config, exc_iface=implementedBy(RuntimeError)) |
| | | self.assertTrue(IMultiView.providedBy(exc_wrapper)) |
| | | self.assertEqual([x[:2] for x in exc_wrapper.views], [(view2, None)]) |
| | | self.assertEqual([(x[0], x[2]) for x in exc_wrapper.views], [(view2, None)]) |
| | | self.assertEqual(exc_wrapper(None, None), 'OK1') |
| | | |
| | | def test_add_view_exc_multiview_replaces_only_exc_multiview(self): |
| | |
| | | exc_wrapper = self._getViewCallable( |
| | | config, exc_iface=implementedBy(RuntimeError)) |
| | | self.assertTrue(IMultiView.providedBy(exc_wrapper)) |
| | | self.assertEqual([x[:2] for x in exc_wrapper.views], [(view2, None)]) |
| | | self.assertEqual([(x[0], x[2]) for x in exc_wrapper.views], [(view2, None)]) |
| | | self.assertEqual(exc_wrapper(None, None), 'OK1') |
| | | |
| | | def test_add_view_multiview_context_superclass_then_subclass(self): |
| | |
| | | config.add_view(view=view, accept='text/xml', renderer=null_renderer) |
| | | wrapper = self._getViewCallable(config) |
| | | request = self._makeRequest(config) |
| | | request.accept = ['text/xml'] |
| | | request.accept = DummyAccept('text/xml') |
| | | self.assertEqual(wrapper(None, request), 'OK') |
| | | |
| | | def test_add_view_with_accept_nomatch(self): |
| | |
| | | config.add_view(view=view, accept='text/xml') |
| | | wrapper = self._getViewCallable(config) |
| | | request = self._makeRequest(config) |
| | | request.accept = ['text/html'] |
| | | request.accept = DummyAccept('text/html') |
| | | self._assertNotFound(wrapper, None, request) |
| | | |
| | | def test_add_view_with_range_accept_match(self): |
| | | from pyramid.renderers import null_renderer |
| | | view = lambda *arg: 'OK' |
| | | config = self._makeOne(autocommit=True) |
| | | config.add_view(view=view, accept='text/*', renderer=null_renderer) |
| | | wrapper = self._getViewCallable(config) |
| | | request = self._makeRequest(config) |
| | | request.accept = DummyAccept('text/html', contains=True) |
| | | self.assertEqual(wrapper(None, request), 'OK') |
| | | |
| | | def test_add_view_with_range_accept_nomatch(self): |
| | | view = lambda *arg: 'OK' |
| | | config = self._makeOne(autocommit=True) |
| | | config.add_view(view=view, accept='text/*') |
| | | wrapper = self._getViewCallable(config) |
| | | request = self._makeRequest(config) |
| | | request.accept = DummyAccept('application/json', contains=False) |
| | | self._assertNotFound(wrapper, None, request) |
| | | |
| | | def test_add_view_with_containment_true(self): |
| | |
| | | request.exception = Exception() |
| | | self.assertEqual(derived_view(None, request), 'OK') |
| | | |
| | | def test_add_view_does_not_accept_iterable_accept(self): |
| | | from pyramid.exceptions import ConfigurationError |
| | | config = self._makeOne(autocommit=True) |
| | | self.assertRaises( |
| | | ConfigurationError, config.add_view, accept=['image/*', 'text/*'], |
| | | ) |
| | | |
| | | def test_default_accept_view_order(self): |
| | | from pyramid.interfaces import IAcceptOrder |
| | | config = self._makeOne(autocommit=True) |
| | | order = config.registry.getUtility(IAcceptOrder) |
| | | result = [v for _, v in order.sorted()] |
| | | self.assertEqual(result, [ |
| | | 'text/html', |
| | | 'application/xhtml+xml', |
| | | 'application/xml', |
| | | 'text/xml', |
| | | 'text/plain', |
| | | 'application/json', |
| | | ]) |
| | | |
| | | def test_add_accept_view_order_override(self): |
| | | from pyramid.interfaces import IAcceptOrder |
| | | config = self._makeOne(autocommit=False) |
| | | config.add_accept_view_order( |
| | | 'text/html', |
| | | weighs_more_than='text/xml', |
| | | weighs_less_than='application/xml', |
| | | ) |
| | | config.commit() |
| | | order = config.registry.getUtility(IAcceptOrder) |
| | | result = [v for _, v in order.sorted()] |
| | | self.assertEqual(result, [ |
| | | 'application/xhtml+xml', |
| | | 'application/xml', |
| | | 'text/html', |
| | | 'text/xml', |
| | | 'text/plain', |
| | | 'application/json', |
| | | ]) |
| | | |
| | | def test_add_accept_view_order_throws_on_wildcard(self): |
| | | config = self._makeOne(autocommit=True) |
| | | self.assertRaises( |
| | | ValueError, config.add_accept_view_order, '*/*', |
| | | ) |
| | | |
| | | def test_add_accept_view_order_throws_on_type_mismatch(self): |
| | | config = self._makeOne(autocommit=True) |
| | | self.assertRaises( |
| | | ValueError, config.add_accept_view_order, |
| | | 'text/*', weighs_more_than='text/html', |
| | | ) |
| | | self.assertRaises( |
| | | ValueError, config.add_accept_view_order, |
| | | 'text/html', weighs_less_than='application/*', |
| | | ) |
| | | self.assertRaises( |
| | | ConfigurationError, config.add_accept_view_order, |
| | | 'text/html', weighs_more_than='text/html;charset=utf8', |
| | | ) |
| | | self.assertRaises( |
| | | ConfigurationError, config.add_accept_view_order, |
| | | 'text/html;charset=utf8', |
| | | weighs_more_than='text/plain;charset=utf8', |
| | | ) |
| | | |
| | | class Test_runtime_exc_view(unittest.TestCase): |
| | | def _makeOne(self, view1, view2): |
| | | from pyramid.config.views import runtime_exc_view |
| | |
| | | self.assertEqual(mv.views, [(100, 'view', None)]) |
| | | mv.add('view2', 99) |
| | | self.assertEqual(mv.views, [(99, 'view2', None), (100, 'view', None)]) |
| | | mv.add('view3', 100, 'text/html') |
| | | mv.add('view3', 100, accept='text/html') |
| | | self.assertEqual(mv.media_views['text/html'], [(100, 'view3', None)]) |
| | | mv.add('view4', 99, 'text/html', 'abc') |
| | | mv.add('view4', 99, 'abc', accept='text/html') |
| | | self.assertEqual(mv.media_views['text/html'], |
| | | [(99, 'view4', 'abc'), (100, 'view3', None)]) |
| | | mv.add('view5', 100, 'text/xml') |
| | | mv.add('view5', 100, accept='text/xml') |
| | | self.assertEqual(mv.media_views['text/xml'], [(100, 'view5', None)]) |
| | | self.assertEqual(set(mv.accepts), set(['text/xml', 'text/html'])) |
| | | self.assertEqual(mv.views, [(99, 'view2', None), (100, 'view', None)]) |
| | | mv.add('view6', 98, 'text/*') |
| | | self.assertEqual(mv.views, [(98, 'view6', None), |
| | | (99, 'view2', None), |
| | | (100, 'view', None)]) |
| | | mv.add('view6', 98, accept='text/*') |
| | | self.assertEqual(mv.views, [ |
| | | (98, 'view6', None), (99, 'view2', None), (100, 'view', None)]) |
| | | |
| | | def test_add_with_phash(self): |
| | | mv = self._makeOne() |
| | |
| | | pass |
| | | |
| | | class DummyAccept(object): |
| | | def __init__(self, *matches): |
| | | def __init__(self, *matches, **kw): |
| | | self.matches = list(matches) |
| | | self.contains = kw.pop('contains', False) |
| | | |
| | | def best_match(self, offered): |
| | | if self.matches: |
| | | for match in self.matches: |
| | | if match in offered: |
| | | self.matches.remove(match) |
| | | return match |
| | | def __contains__(self, val): |
| | | return val in self.matches |
| | | def acceptable_offers(self, offers): |
| | | results = [] |
| | | for match in self.matches: |
| | | if match in offers: |
| | | results.append((match, 1.0)) |
| | | return results |
| | | |
| | | def __contains__(self, value): |
| | | return self.contains |
| | | |
| | | class DummyConfig: |
| | | def __init__(self): |
| | |
| | | def __init__(self): |
| | | self.views = [] |
| | | self.name = 'name' |
| | | def add(self, view, order, accept=None, phash=None): |
| | | self.views.append((view, accept, phash)) |
| | | def add(self, view, order, phash=None, accept=None, accept_order=None): |
| | | self.views.append((view, phash, accept, accept_order)) |
| | | def __call__(self, context, request): |
| | | return 'OK1' |
| | | def __permitted__(self, context, request): |
| | |
| | | ) |
| | | |
| | | from zope.interface import Interface |
| | | from webtest import TestApp |
| | | |
| | | # 5 years from now (more or less) |
| | | fiveyrsfuture = datetime.datetime.utcnow() + datetime.timedelta(5*365) |
| | |
| | | package=self.package) |
| | | config.include(self.package) |
| | | app = config.make_wsgi_app() |
| | | from webtest import TestApp |
| | | self.testapp = TestApp(app) |
| | | self.config = config |
| | | |
| | |
| | | config = self._makeConfig() |
| | | config.include(self.package) |
| | | app = config.make_wsgi_app() |
| | | from webtest import TestApp |
| | | self.testapp = TestApp(app) |
| | | res = self.testapp.get('/') |
| | | self.assertTrue(b'a view' in res.body) |
| | |
| | | return Response('this view') |
| | | config.add_view(thisview) |
| | | app = config.make_wsgi_app() |
| | | from webtest import TestApp |
| | | self.testapp = TestApp(app) |
| | | res = self.testapp.get('/') |
| | | self.assertTrue(b'this view' in res.body) |
| | |
| | | return Response('this view') |
| | | config.add_view(thisview, route_name='aroute') |
| | | app = config.make_wsgi_app() |
| | | from webtest import TestApp |
| | | self.testapp = TestApp(app) |
| | | res = self.testapp.get('/route') |
| | | self.assertTrue(b'this view' in res.body) |
| | |
| | | config = self._makeConfig() |
| | | config.include(self.package) |
| | | app = config.make_wsgi_app() |
| | | from webtest import TestApp |
| | | self.testapp = TestApp(app) |
| | | res = self.testapp.get('/protected', status=403) |
| | | self.assertTrue(b'403 Forbidden' in res.body) |
| | |
| | | config.set_authorization_policy(DummySecurityPolicy('fred')) |
| | | config.set_authentication_policy(DummySecurityPolicy(permissive=True)) |
| | | app = config.make_wsgi_app() |
| | | from webtest import TestApp |
| | | self.testapp = TestApp(app) |
| | | res = self.testapp.get('/protected', status=200) |
| | | self.assertTrue('protected view' in res) |
| | |
| | | from pyramid.tests.pkgs.includeapp1.root import configure |
| | | configure(config) |
| | | app = config.make_wsgi_app() |
| | | from webtest import TestApp |
| | | self.testapp = TestApp(app) |
| | | self.config = config |
| | | |
| | |
| | | from pyramid.tests.test_config.pkgs.selfscan import main |
| | | config = main() |
| | | app = config.make_wsgi_app() |
| | | from webtest import TestApp |
| | | self.testapp = TestApp(app) |
| | | self.config = config |
| | | |
| | |
| | | from pyramid.tests.pkgs.wsgiapp2app import main |
| | | config = main() |
| | | app = config.make_wsgi_app() |
| | | from webtest import TestApp |
| | | self.testapp = TestApp(app) |
| | | self.config = config |
| | | |
| | |
| | | from pyramid.tests.pkgs.subrequestapp import main |
| | | config = main() |
| | | app = config.make_wsgi_app() |
| | | from webtest import TestApp |
| | | self.testapp = TestApp(app) |
| | | self.config = config |
| | | |
| | |
| | | def test_rescan(self): |
| | | self.config.scan('pyramid.tests.pkgs.rendererscanapp') |
| | | app = self.config.make_wsgi_app() |
| | | from webtest import TestApp |
| | | testapp = TestApp(app) |
| | | res = testapp.get('/one', status=200) |
| | | self.assertTrue(b'One!' in res.body) |
| | |
| | | return config |
| | | |
| | | def _makeTestApp(self, config): |
| | | from webtest import TestApp |
| | | app = config.make_wsgi_app() |
| | | return TestApp(app) |
| | | |
| | |
| | | |
| | | |
| | | class AcceptContentTypeTest(unittest.TestCase): |
| | | def setUp(self): |
| | | def _makeConfig(self): |
| | | def hello_view(request): |
| | | return {'message': 'Hello!'} |
| | | from pyramid.config import Configurator |
| | | config = Configurator() |
| | | config.add_route('hello', '/hello') |
| | | config.add_view(hello_view, route_name='hello', accept='text/plain', renderer='string') |
| | | config.add_view(hello_view, route_name='hello', accept='application/json', renderer='json') |
| | | config.add_view(hello_view, route_name='hello', |
| | | accept='text/plain', renderer='string') |
| | | config.add_view(hello_view, route_name='hello', |
| | | accept='application/json', renderer='json') |
| | | def hello_fallback_view(request): |
| | | request.response.content_type = 'text/x-fallback' |
| | | return 'hello fallback' |
| | | config.add_view(hello_fallback_view, route_name='hello', |
| | | renderer='string') |
| | | return config |
| | | |
| | | def _makeTestApp(self, config): |
| | | app = config.make_wsgi_app() |
| | | from webtest import TestApp |
| | | self.testapp = TestApp(app) |
| | | return TestApp(app) |
| | | |
| | | def tearDown(self): |
| | | import pyramid.config |
| | | pyramid.config.global_registries.empty() |
| | | pyramid.config.global_registries.empty() |
| | | |
| | | def test_ordering(self): |
| | | res = self.testapp.get('/hello', headers={'Accept': 'application/json; q=1.0, text/plain; q=0.9'}, status=200) |
| | | def test_client_side_ordering(self): |
| | | config = self._makeConfig() |
| | | app = self._makeTestApp(config) |
| | | res = app.get('/hello', headers={ |
| | | 'Accept': 'application/json; q=1.0, text/plain; q=0.9', |
| | | }, status=200) |
| | | self.assertEqual(res.content_type, 'application/json') |
| | | res = self.testapp.get('/hello', headers={'Accept': 'text/plain; q=0.9, application/json; q=1.0'}, status=200) |
| | | res = app.get('/hello', headers={ |
| | | 'Accept': 'text/plain; q=0.9, application/json; q=1.0', |
| | | }, status=200) |
| | | self.assertEqual(res.content_type, 'application/json') |
| | | |
| | | def test_wildcards(self): |
| | | res = self.testapp.get('/hello', headers={'Accept': 'application/*'}, status=200) |
| | | res = app.get('/hello', headers={'Accept': 'application/*'}, status=200) |
| | | self.assertEqual(res.content_type, 'application/json') |
| | | res = self.testapp.get('/hello', headers={'Accept': 'text/*'}, status=200) |
| | | res = app.get('/hello', headers={'Accept': 'text/*'}, status=200) |
| | | self.assertEqual(res.content_type, 'text/plain') |
| | | res = app.get('/hello', headers={'Accept': 'something/else'}, status=200) |
| | | self.assertEqual(res.content_type, 'text/x-fallback') |
| | | |
| | | def test_default_server_side_ordering(self): |
| | | config = self._makeConfig() |
| | | app = self._makeTestApp(config) |
| | | res = app.get('/hello', headers={ |
| | | 'Accept': 'application/json, text/plain', |
| | | }, status=200) |
| | | self.assertEqual(res.content_type, 'text/plain') |
| | | res = app.get('/hello', headers={ |
| | | 'Accept': 'text/plain, application/json', |
| | | }, status=200) |
| | | self.assertEqual(res.content_type, 'text/plain') |
| | | res = app.get('/hello', headers={'Accept': '*/*'}, status=200) |
| | | self.assertEqual(res.content_type, 'text/plain') |
| | | res = app.get('/hello', status=200) |
| | | self.assertEqual(res.content_type, 'text/plain') |
| | | res = app.get('/hello', headers={'Accept': 'invalid'}, status=200) |
| | | self.assertEqual(res.content_type, 'text/plain') |
| | | res = app.get('/hello', headers={'Accept': 'something/else'}, status=200) |
| | | self.assertEqual(res.content_type, 'text/x-fallback') |
| | | |
| | | def test_custom_server_side_ordering(self): |
| | | config = self._makeConfig() |
| | | config.add_accept_view_order( |
| | | 'application/json', weighs_more_than='text/plain') |
| | | app = self._makeTestApp(config) |
| | | res = app.get('/hello', headers={ |
| | | 'Accept': 'application/json, text/plain', |
| | | }, status=200) |
| | | self.assertEqual(res.content_type, 'application/json') |
| | | res = app.get('/hello', headers={ |
| | | 'Accept': 'text/plain, application/json', |
| | | }, status=200) |
| | | self.assertEqual(res.content_type, 'application/json') |
| | | res = app.get('/hello', headers={'Accept': '*/*'}, status=200) |
| | | self.assertEqual(res.content_type, 'application/json') |
| | | res = app.get('/hello', status=200) |
| | | self.assertEqual(res.content_type, 'application/json') |
| | | res = app.get('/hello', headers={'Accept': 'invalid'}, status=200) |
| | | self.assertEqual(res.content_type, 'application/json') |
| | | res = app.get('/hello', headers={'Accept': 'something/else'}, status=200) |
| | | self.assertEqual(res.content_type, 'text/x-fallback') |
| | | |
| | | def test_deprecated_ranges_in_route_predicate(self): |
| | | config = self._makeConfig() |
| | | config.add_route('foo', '/foo', accept='text/*') |
| | | config.add_view(lambda r: 'OK', route_name='foo', renderer='string') |
| | | app = self._makeTestApp(config) |
| | | res = app.get('/foo', headers={ |
| | | 'Accept': 'application/json; q=1.0, text/plain; q=0.9', |
| | | }, status=200) |
| | | self.assertEqual(res.content_type, 'text/plain') |
| | | self.assertEqual(res.body, b'OK') |
| | | res = app.get('/foo', headers={ |
| | | 'Accept': 'application/json', |
| | | }, status=404) |
| | | self.assertEqual(res.content_type, 'application/json') |
| | | |
| | | def test_deprecated_ranges_in_view_predicate(self): |
| | | config = self._makeConfig() |
| | | config.add_route('foo', '/foo') |
| | | config.add_view(lambda r: 'OK', route_name='foo', |
| | | accept='text/*', renderer='string') |
| | | app = self._makeTestApp(config) |
| | | res = app.get('/foo', headers={ |
| | | 'Accept': 'application/json; q=1.0, text/plain; q=0.9', |
| | | }, status=200) |
| | | self.assertEqual(res.content_type, 'text/plain') |
| | | self.assertEqual(res.body, b'OK') |
| | | res = app.get('/foo', headers={ |
| | | 'Accept': 'application/json', |
| | | }, status=404) |
| | | self.assertEqual(res.content_type, 'application/json') |
| | | |
| | | |
| | | class DummyContext(object): |
| | |
| | | # W293: blank line contains whitespace |
| | | W293, |
| | | # W391: blank line at end of file |
| | | W391 |
| | | W391, |
| | | # W503: line break before binary operator |
| | | W503 |
| | | exclude = pyramid/tests/,pyramid/compat.py,pyramid/resource.py |
| | | show-source = True |
| | | |
| | |
| | | 'setuptools', |
| | | 'translationstring >= 0.4', # py3 compat |
| | | 'venusian >= 1.0', # ``ignore`` |
| | | 'webob >= 1.8.2', # cookies.make_cookie allows non-bytes samesite |
| | | 'webob >= 1.8.3', # Accept.parse_offer |
| | | 'zope.deprecation >= 3.5.0', # py3 compat |
| | | 'zope.interface >= 3.8.0', # has zope.interface.registry |
| | | ] |