Michael Merickel
2018-10-10 4a9f4f43684c3a754f43935b97013057340c305d
deprecate range support
10 files modified
501 ■■■■■ changed files
CHANGES.rst 11 ●●●●● patch | view | raw | blame | history
docs/narr/viewconfig.rst 14 ●●●● patch | view | raw | blame | history
pyramid/config/routes.py 36 ●●●● patch | view | raw | blame | history
pyramid/config/util.py 47 ●●●●● patch | view | raw | blame | history
pyramid/config/views.py 61 ●●●●● patch | view | raw | blame | history
pyramid/predicates.py 16 ●●●● patch | view | raw | blame | history
pyramid/tests/test_config/test_routes.py 18 ●●●●● patch | view | raw | blame | history
pyramid/tests/test_config/test_util.py 27 ●●●●● patch | view | raw | blame | history
pyramid/tests/test_config/test_views.py 35 ●●●● patch | view | raw | blame | history
pyramid/tests/test_integration.py 236 ●●●● patch | view | raw | blame | history
CHANGES.rst
@@ -113,6 +113,17 @@
  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
--------------------------
docs/narr/viewconfig.rst
@@ -1093,23 +1093,17 @@
~~~~~~~~~~~~~~~~~~~~~~~
:app:`Pyramid` will always sort multiple views with the same ``(name, context, route_name)`` first by the specificity of the ``accept`` offer.
This means that ``text/plain`` will always be offered before ``text/*``.
Similarly ``text/plain;charset=utf8`` will always be offered before ``text/plain``.
The following order is always preserved between the following offers (more preferred to less preferred):
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``.
- ``type/subtype;params``
- ``type/subtype``
- ``type/*``
- ``*/*``
Within each of these levels of specificity, the ordering is ambiguous and 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:
By default, within a given ``type/subtype``, the order of offers is ambiguous. For example, ``text/plain;charset=utf8`` versus ``text/plain;charset=latin1`` are sorted in an unspecified way. 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/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/*``. That ordering is a hard requirement.
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:
pyramid/config/routes.py
@@ -22,6 +22,7 @@
from pyramid.config.util import (
    action_method,
    normalize_accept_offer,
    predvalseq,
)
@@ -228,15 +229,23 @@
          A :term:`media type` that will be matched against the ``Accept``
          HTTP request header.  This value may be a specific media type such
          as ``text/html``, or a range like ``text/*``, 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.
          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.
          .. 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
@@ -297,9 +306,22 @@
        if accept is not None:
            if not is_nonstr_iter(accept):
                accept = [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 verify=True when range support is dropped
                accept = [normalize_accept_offer(accept, verify=False)]
            accept = [accept_option.lower() for accept_option in accept]
            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
pyramid/config/util.py
@@ -221,16 +221,18 @@
        return order, preds, phash.hexdigest()
def normalize_accept_offer(offer, verify=True):
    if verify:
        Accept.parse_offer(offer)
    return offer.lower()
def sort_accept_offers(offers, order=None):
    """
    Sort a list of offers by specificity and preference.
    Sort a list of offers by preference.
    Supported offers are of the following forms, ordered by specificity
    (higher to lower):
    - ``type/subtype;params`` and ``type/subtype``
    - ``type/*``
    - ``*/*``
    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
@@ -249,20 +251,10 @@
    def offer_sort_key(value):
        """
        (category, type_weight, params_weight)
        category:
            1 - foo/bar and foo/bar;params
            2 - foo/*
            3 - */*
        (type_weight, params_weight)
        type_weight:
            if category 1 & 2:
                - index of type/* in order list
                - ``max_weight`` if no match is found
            - index of type/subtype in order list
            - index of type/* in order list + ``max_weight``
            - index of specific ``type/subtype`` in order list
            - ``max_weight * 2`` if no match is found
        params_weight:
@@ -273,17 +265,10 @@
        """
        parsed = Accept.parse_offer(value)
        if value == '*/*':
            return (3, 0, 0)
        elif parsed.subtype == '*':
            type_w = find_order_index(value, max_weight)
            return (2, type_w, 0)
        type_w = find_order_index(parsed.type + '/' + parsed.subtype, None)
        if type_w is None:
            type_w = max_weight + find_order_index(
                parsed.type + '/*', max_weight)
        type_w = find_order_index(
            parsed.type + '/' + parsed.subtype,
            max_weight,
        )
        if parsed.params:
            param_w = find_order_index(value, max_weight)
@@ -291,6 +276,6 @@
        else:
            param_w = max_weight + 1
        return (1, type_w, param_w)
        return (type_w, param_w)
    return sorted(offers, key=offer_sort_key)
pyramid/config/views.py
@@ -88,6 +88,7 @@
    action_method,
    DEFAULT_PHASH,
    MAX_ORDER,
    normalize_accept_offer,
    predvalseq,
    sort_accept_offers,
    )
@@ -125,7 +126,7 @@
                    self.views[i] = (order, view, phash)
                    return
        if accept is None:
        if accept is None or '*' in accept:
            self.views.append((order, view, phash))
            self.views.sort(key=operator.itemgetter(0))
        else:
@@ -676,8 +677,8 @@
        accept
          A :term:`media type` that will be matched against the ``Accept``
          HTTP request header.  This value may be a specific media type such
          as ``text/html``, or a range like ``text/*``.  If the media type is
          HTTP request header.  This value 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
@@ -688,6 +689,12 @@
          the associated view callable.
          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.
        path_info
@@ -821,7 +828,17 @@
                raise ConfigurationError(
                    'A list is not supported in the "accept" view predicate.',
                )
            accept = accept.lower()
            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 verify=True
            accept = normalize_accept_offer(accept, verify=False)
        view = self.maybe_dotted(view)
        context = self.maybe_dotted(context)
@@ -1273,46 +1290,38 @@
        .. versionadded:: 1.10
        """
        if value == '*/*':
            raise ConfigurationError(
                'cannot specify an ordering for an offer of */*')
        def normalize_type(type):
            return type.lower()
        def check_type(than):
            than_type, than_subtype, than_params = Accept.parse_offer(than)
            if (
                # text/* vs text/plain
                (offer_subtype == '*') ^ (than_subtype == '*')
                # text/plain vs text/html;charset=utf8
                or (bool(offer_params) ^ bool(than_params))
            ):
            # text/plain vs text/html;charset=utf8
            if bool(offer_params) ^ bool(than_params):
                raise ConfigurationError(
                    'cannot compare across media range specificity levels')
                    '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 media types')
                    'cannot compare params across different media types')
        value = normalize_type(value)
        def normalize_types(thans):
            thans = [normalize_accept_offer(o, verify=False) for o in thans]
            for o in thans:
                check_type(o)
            return thans
        value = normalize_accept_offer(value, verify=False)
        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_type(w) for w in weighs_more_than]
            for than in weighs_more_than:
                check_type(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_type(w) for w in weighs_less_than]
            for than in weighs_less_than:
                check_type(than)
            weighs_less_than = normalize_types(weighs_less_than)
        discriminator = ('accept view order', value)
        intr = self.introspectable(
pyramid/predicates.py
@@ -130,10 +130,16 @@
        return self.val.match(val) is not None
class AcceptPredicate(object):
    def __init__(self, val, config):
        if not is_nonstr_iter(val):
            val = (val,)
        self.values = 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' % (', '.join(self.values),)
@@ -141,6 +147,8 @@
    phash = text
    def __call__(self, context, request):
        if self._is_using_deprecated_ranges:
            return self.values[0] in request.accept
        return bool(request.accept.acceptable_offers(self.values))
class ContainmentPredicate(object):
pyramid/tests/test_config/test_routes.py
@@ -203,6 +203,18 @@
        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):
        config = self._makeOne(autocommit=True)
        config.add_route('name', path='path')
@@ -270,8 +282,9 @@
        self.cookies = {}
class DummyAccept(object):
    def __init__(self, *matches):
    def __init__(self, *matches, **kw):
        self.matches = list(matches)
        self.contains = kw.pop('contains', False)
    def acceptable_offers(self, offers):
        results = []
@@ -279,3 +292,6 @@
            if match in offers:
                results.append((match, 1.0))
        return results
    def __contains__(self, value):
        return self.contains
pyramid/tests/test_config/test_util.py
@@ -437,17 +437,10 @@
        return sort_accept_offers(offers, order)
    def test_default_specificities(self):
        result = self._callFUT(['*/*', 'text/*', 'text/html', 'text/html;charset=utf8'])
        result = self._callFUT(['text/html', 'text/html;charset=utf8'])
        self.assertEqual(result, [
            'text/html;charset=utf8', 'text/html', 'text/*', '*/*',
            'text/html;charset=utf8', 'text/html',
        ])
    def test_wildcard_type_order(self):
        result = self._callFUT(
            ['*/*', 'text/*', 'image/*'],
            ['image/*', 'text/*'],
        )
        self.assertEqual(result, ['image/*', 'text/*', '*/*'])
    def test_specific_type_order(self):
        result = self._callFUT(
@@ -473,22 +466,6 @@
            ['text/plain', 'text/html'],
        )
        self.assertEqual(result, ['text/plain;charset=latin1', 'text/html;charset=utf8'])
    def test_params_inherit_wildcard_prefs(self):
        result = self._callFUT(
            ['image/png;progressive=1', 'text/html;charset=utf8'],
            ['text/*', 'image/*'],
        )
        self.assertEqual(result, ['text/html;charset=utf8', 'image/png;progressive=1'])
    def test_type_overrides_wildcard_prefs(self):
        result = self._callFUT(
            ['text/html;charset=utf8', 'image/png', 'foo/bar', 'text/bar'],
            ['foo/*', 'text/*', 'image/*', 'image/png', 'text/html'],
        )
        self.assertEqual(result, [
            'image/png', 'text/html;charset=utf8', 'foo/bar', 'text/bar',
        ])
class DummyCustomPredicate(object):
    def __init__(self):
pyramid/tests/test_config/test_views.py
@@ -1477,6 +1477,25 @@
        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):
        from pyramid.renderers import null_renderer
        from zope.interface import directlyProvides
@@ -2431,20 +2450,19 @@
        ])
    def test_add_accept_view_order_throws_on_wildcard(self):
        from pyramid.exceptions import ConfigurationError
        config = self._makeOne(autocommit=True)
        self.assertRaises(
            ConfigurationError, config.add_accept_view_order, '*/*',
            ValueError, config.add_accept_view_order, '*/*',
        )
    def test_add_accept_view_order_throws_on_type_mismatch(self):
        config = self._makeOne(autocommit=True)
        self.assertRaises(
            ConfigurationError, config.add_accept_view_order,
            ValueError, config.add_accept_view_order,
            'text/*', weighs_more_than='text/html',
        )
        self.assertRaises(
            ConfigurationError, config.add_accept_view_order,
            ValueError, config.add_accept_view_order,
            'text/html', weighs_less_than='application/*',
        )
        self.assertRaises(
@@ -2577,7 +2595,8 @@
        self.assertEqual(set(mv.accepts), set(['text/xml', 'text/html']))
        self.assertEqual(mv.views, [(99, 'view2', None), (100, 'view', None)])
        mv.add('view6', 98, accept='text/*')
        self.assertEqual(mv.media_views['text/*'], [(98, 'view6', None)])
        self.assertEqual(mv.views, [
            (98, 'view6', None), (99, 'view2', None), (100, 'view', None)])
    def test_add_with_phash(self):
        mv = self._makeOne()
@@ -3503,8 +3522,9 @@
    pass
class DummyAccept(object):
    def __init__(self, *matches):
    def __init__(self, *matches, **kw):
        self.matches = list(matches)
        self.contains = kw.pop('contains', False)
    def acceptable_offers(self, offers):
        results = []
@@ -3513,6 +3533,9 @@
                results.append((match, 1.0))
        return results
    def __contains__(self, value):
        return self.contains
class DummyConfig:
    def __init__(self):
        self.registry = DummyRegistry()
pyramid/tests/test_integration.py
@@ -759,219 +759,37 @@
        res = app.get('/hello', headers={'Accept': 'something/else'}, status=200)
        self.assertEqual(res.content_type, 'text/x-fallback')
class AddViewAcceptArgMediaRangeAllTest(unittest.TestCase):
    def setUp(self):
        def view(request):
            return 'text/plain'
        from pyramid.config import Configurator
        config = Configurator()
        config.add_route('root', '/')
        config.add_view(
            view, route_name='root', accept='*/*', renderer='string',
        )
        app = config.make_wsgi_app()
        self.testapp = TestApp(app)
    def tearDown(self):
        import pyramid.config
        pyramid.config.global_registries.empty()
    def test_no_header(self):
        res = self.testapp.get('/', headers={}, status=200)
    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_header_all(self):
        res = self.testapp.get('/', headers={'Accept': '*/*'}, status=200)
    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')
    def test_header_all_subtypes_of_type(self):
        res = self.testapp.get('/', headers={'Accept': 'text/*'}, status=200)
        self.assertEqual(res.content_type, 'text/plain')
    def test_header_specific_media_type(self):
        res = self.testapp.get(
            '/', headers={'Accept': 'text/plain'}, status=200,
        )
        self.assertEqual(res.content_type, 'text/plain')
    def test_header_ruled_out_by_specific_media_type_q0(self):
        res = self.testapp.get(
            '/', headers={'Accept': 'text/plain;q=0, */*'}, status=200,
        )
        self.assertEqual(res.content_type, 'text/plain')
    def test_header_ruled_out_by_type_range_q0(self):
        res = self.testapp.get(
            '/', headers={'Accept': 'text/*;q=0, text/html'}, status=200,
        )
        self.assertEqual(res.content_type, 'text/plain')
    def test_header_ruled_out_by_all_range_q0(self):
        res = self.testapp.get(
            '/', headers={'Accept': '*/*;q=0, text/html'}, status=200,
        )
        self.assertEqual(res.content_type, 'text/plain')
class AddViewAcceptArgMediaRangeAllSubtypesOfTypeTest(unittest.TestCase):
    def setUp(self):
        def view(request):
            return 'text/plain'
        from pyramid.config import Configurator
        config = Configurator()
        config.add_route('root', '/')
        config.add_view(
            view, route_name='root', accept='text/*', renderer='string',
        )
        app = config.make_wsgi_app()
        self.testapp = TestApp(app)
    def tearDown(self):
        import pyramid.config
        pyramid.config.global_registries.empty()
    def test_no_header(self):
        res = self.testapp.get('/', headers={}, status=200)
        self.assertEqual(res.content_type, 'text/plain')
    def test_header_all(self):
        res = self.testapp.get('/', headers={'Accept': '*/*'}, status=200)
        self.assertEqual(res.content_type, 'text/plain')
    def test_header_all_subtypes_of_type(self):
        res = self.testapp.get('/', headers={'Accept': 'text/*'}, status=200)
        self.assertEqual(res.content_type, 'text/plain')
    def test_header_specific_media_type(self):
        res = self.testapp.get(
            '/', headers={'Accept': 'text/plain'}, status=200,
        )
        self.assertEqual(res.content_type, 'text/plain')
    def test_header_none_acceptable(self):
        self.testapp.get('/', headers={'Accept': 'application/*'}, status=404)
    def test_header_ruled_out_by_specific_media_type_q0(self):
        res = self.testapp.get(
            '/', headers={'Accept': 'text/plain;q=0, */*'}, status=200,
        )
        self.assertEqual(res.content_type, 'text/plain')
    def test_header_ruled_out_by_type_range_q0(self):
        res = self.testapp.get(
            '/', headers={'Accept': 'text/*;q=0, text/html'}, status=200,
        )
        self.assertEqual(res.content_type, 'text/plain')
    def test_header_ruled_out_by_all_range_q0(self):
        res = self.testapp.get(
            '/', headers={'Accept': '*/*;q=0, text/html'}, status=200,
        )
        self.assertEqual(res.content_type, 'text/plain')
class AddRouteAcceptArgMediaRangeAllTest(unittest.TestCase):
    def setUp(self):
        def view(request):
            return 'text/plain'
        from pyramid.config import Configurator
        config = Configurator()
        config.add_route('root', '/', accept='*/*')
        config.add_view(view, route_name='root', renderer='string')
        app = config.make_wsgi_app()
        self.testapp = TestApp(app)
    def tearDown(self):
        import pyramid.config
        pyramid.config.global_registries.empty()
    def test_no_header(self):
        res = self.testapp.get('/', headers={}, status=200)
        self.assertEqual(res.content_type, 'text/plain')
    def test_header_all(self):
        res = self.testapp.get('/', headers={'Accept': '*/*'}, status=200)
        self.assertEqual(res.content_type, 'text/plain')
    def test_header_all_subtypes_of_type(self):
        res = self.testapp.get('/', headers={'Accept': 'text/*'}, status=200)
        self.assertEqual(res.content_type, 'text/plain')
    def test_header_specific_media_type(self):
        res = self.testapp.get(
            '/', headers={'Accept': 'text/plain'}, status=200,
        )
        self.assertEqual(res.content_type, 'text/plain')
    def test_header_ruled_out_by_specific_media_type_q0(self):
        res = self.testapp.get(
            '/', headers={'Accept': 'text/plain;q=0, */*'}, status=200,
        )
        self.assertEqual(res.content_type, 'text/plain')
    def test_header_ruled_out_by_type_range_q0(self):
        res = self.testapp.get(
            '/', headers={'Accept': 'text/*;q=0, text/html'}, status=200,
        )
        self.assertEqual(res.content_type, 'text/plain')
    def test_header_ruled_out_by_all_range_q0(self):
        res = self.testapp.get(
            '/', headers={'Accept': '*/*;q=0, text/html'}, status=200,
        )
        self.assertEqual(res.content_type, 'text/plain')
class AddRouteAcceptArgMediaRangeAllSubtypesOfTypeTest(unittest.TestCase):
    def setUp(self):
        def view(request):
            return 'text/plain'
        from pyramid.config import Configurator
        config = Configurator()
        config.add_route('root', '/', accept='text/*')
        config.add_view(view, route_name='root', renderer='string')
        app = config.make_wsgi_app()
        self.testapp = TestApp(app)
    def tearDown(self):
        import pyramid.config
        pyramid.config.global_registries.empty()
    def test_no_header(self):
        res = self.testapp.get('/', headers={}, status=200)
        self.assertEqual(res.content_type, 'text/plain')
    def test_header_all(self):
        res = self.testapp.get('/', headers={'Accept': '*/*'}, status=200)
        self.assertEqual(res.content_type, 'text/plain')
    def test_header_all_subtypes_of_type(self):
        res = self.testapp.get('/', headers={'Accept': 'text/*'}, status=200)
        self.assertEqual(res.content_type, 'text/plain')
    def test_header_specific_media_type(self):
        res = self.testapp.get(
            '/', headers={'Accept': 'text/plain'}, status=200,
        )
        self.assertEqual(res.content_type, 'text/plain')
    def test_header_none_acceptable(self):
        self.testapp.get('/', headers={'Accept': 'application/*'}, status=404)
    def test_header_ruled_out_by_specific_media_type_q0(self):
        res = self.testapp.get(
            '/', headers={'Accept': 'text/plain;q=0, */*'}, status=200,
        )
        self.assertEqual(res.content_type, 'text/plain')
    def test_header_ruled_out_by_type_range_q0(self):
        res = self.testapp.get(
            '/', headers={'Accept': 'text/*;q=0, text/html'}, status=200,
        )
        self.assertEqual(res.content_type, 'text/plain')
    def test_header_ruled_out_by_all_range_q0(self):
        res = self.testapp.get(
            '/', headers={'Accept': '*/*;q=0, text/html'}, status=200,
        )
        self.assertEqual(res.content_type, 'text/plain')
class DummyContext(object):
    pass