diff options
| -rw-r--r-- | CHANGES.rst | 4 | ||||
| -rw-r--r-- | docs/narr/viewconfig.rst | 6 | ||||
| -rw-r--r-- | src/pyramid/config/routes.py | 3 | ||||
| -rw-r--r-- | src/pyramid/config/views.py | 3 | ||||
| -rw-r--r-- | src/pyramid/predicates.py | 52 | ||||
| -rw-r--r-- | tests/test_config/test_predicates.py | 69 |
6 files changed, 107 insertions, 30 deletions
diff --git a/CHANGES.rst b/CHANGES.rst index 67256db8d..3bd14705d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,10 @@ unreleased Features -------- +- It is now possible to pass multiple values to the ``header`` predicate + for route and view configuration. + See https://github.com/Pylons/pyramid/pull/3576 + - Add support for Python 3.8. See https://github.com/Pylons/pyramid/pull/3547 diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst index 891d294d7..c40f1181a 100644 --- a/docs/narr/viewconfig.rst +++ b/docs/narr/viewconfig.rst @@ -448,10 +448,10 @@ configured view. associated view callable. ``header`` - This value represents an HTTP header name or a header name/value pair. + This value matches one or more HTTP header names or header name/value pairs. - If ``header`` is specified, it must be a header name or a - ``headername:headervalue`` pair. + If ``header`` is specified, it must be a string or a sequence of strings, + each being a header name or a ``headername:headervalue`` pair. If ``header`` is specified without a value (a bare header name only, e.g., ``If-Modified-Since``), the view will only be invoked if the HTTP header diff --git a/src/pyramid/config/routes.py b/src/pyramid/config/routes.py index 7c78fbfa7..5daa1dc00 100644 --- a/src/pyramid/config/routes.py +++ b/src/pyramid/config/routes.py @@ -220,7 +220,8 @@ class RoutesConfiguratorMixin: header This argument represents an HTTP header name or a header - name/value pair. If the argument contains a ``:`` (colon), + name/value pair, or a sequence of them. + If the argument contains a ``:`` (colon), it will be considered a name/value pair (e.g. ``User-Agent:Mozilla/.*`` or ``Host:localhost``). If the value contains a colon, the value portion should be a diff --git a/src/pyramid/config/views.py b/src/pyramid/config/views.py index 466c31f94..9053160fa 100644 --- a/src/pyramid/config/views.py +++ b/src/pyramid/config/views.py @@ -671,7 +671,8 @@ class ViewsConfiguratorMixin: header This value represents an HTTP header name or a header - name/value pair. If the value contains a ``:`` (colon), it + name/value pair, or a sequence of them. + If the value contains a ``:`` (colon), it will be considered a name/value pair (e.g. ``User-Agent:Mozilla/.*`` or ``Host:localhost``). The value portion should be a regular expression. If the value diff --git a/src/pyramid/predicates.py b/src/pyramid/predicates.py index f51ea3b21..0c74ed6d5 100644 --- a/src/pyramid/predicates.py +++ b/src/pyramid/predicates.py @@ -98,33 +98,43 @@ class RequestParamPredicate: class HeaderPredicate: def __init__(self, val, config): - name = val - v = None - if ':' in name: - name, val_str = name.split(':', 1) - try: - v = re.compile(val_str) - except re.error as why: - raise ConfigurationError(why.args[0]) - if v is None: - self._text = 'header %s' % (name,) - else: - self._text = 'header %s=%s' % (name, val_str) - self.name = name - self.val = v + values = [] + + val = as_sorted_tuple(val) + for name in val: + v, val_str = None, None + if ':' in name: + name, val_str = name.split(':', 1) + try: + v = re.compile(val_str) + except re.error as why: + raise ConfigurationError(why.args[0]) + + values.append((name, v, val_str)) + + self.val = values def text(self): - return self._text + return 'header %s' % ', '.join( + '%s=%s' % (name, val_str) if val_str else name + for name, _, val_str in self.val + ) phash = text def __call__(self, context, request): - if self.val is None: - return self.name in request.headers - val = request.headers.get(self.name) - if val is None: - return False - return self.val.match(val) is not None + for name, val, _ in self.val: + if val is None: + if name not in request.headers: + return False + else: + value = request.headers.get(name) + if value is None: + return False + if val.match(value) is None: + return False + + return True class AcceptPredicate: diff --git a/tests/test_config/test_predicates.py b/tests/test_config/test_predicates.py index 31e7a38e7..c0185340f 100644 --- a/tests/test_config/test_predicates.py +++ b/tests/test_config/test_predicates.py @@ -315,14 +315,14 @@ class TestPredicateList(unittest.TestCase): def test_predicate_text_is_correct_when_multiple(self): _, predicates, _ = self._callFUT( request_method=('one', 'two'), - request_param=('param1', 'param2=on'), - header='header:text/*', + request_param=('par2=on', 'par1'), + header=('header2', 'header1:val.*'), accept=('accept1', 'accept2'), match_param=('foo=bar', 'baz=bim'), ) self.assertEqual(predicates[0].text(), "request_method = one,two") - self.assertEqual(predicates[1].text(), 'request_param param1,param2=on') - self.assertEqual(predicates[2].text(), 'header header=text/*') + self.assertEqual(predicates[1].text(), 'request_param par1,par2=on') + self.assertEqual(predicates[2].text(), 'header header1=val.*, header2') self.assertEqual(predicates[3].text(), 'accept = accept1, accept2') self.assertEqual(predicates[4].text(), "match_param baz=bim,foo=bar") @@ -368,6 +368,66 @@ class TestPredicateList(unittest.TestCase): hash2, _, __ = self._callFUT(request_method='GET') self.assertEqual(hash1, hash2) + def test_header_simple(self): + _, predicates, _ = self._callFUT(header='foo') + request = DummyRequest() + request.headers = {'foo': 'bars', 'baz': 'foo'} + self.assertTrue(predicates[0](Dummy(), request)) + + def test_header_simple_fails(self): + _, predicates, _ = self._callFUT(header='content-length') + request = DummyRequest() + request.headers = {'foo': 'bars', 'baz': 'foo'} + self.assertFalse(predicates[0](Dummy(), request)) + + def test_header_with_value(self): + _, predicates, _ = self._callFUT(header='foo:bar') + request = DummyRequest() + request.headers = {'foo': 'bars', 'baz': 'foo'} + self.assertTrue(predicates[0](Dummy(), request)) + + def test_header_with_value_fails(self): + _, predicates, _ = self._callFUT(header='foo:bar') + request = DummyRequest() + request.headers = {'foo': 'nobar', 'baz': 'foo'} + self.assertFalse(predicates[0](Dummy(), request)) + + def test_header_multiple(self): + _, predicates, _ = self._callFUT(header=('foo', 'content-length')) + request = DummyRequest() + request.headers = {'foo': 'bars', 'content-length': '42'} + self.assertTrue(predicates[0](Dummy(), request)) + + def test_header_multiple_fails(self): + _, predicates, _ = self._callFUT(header=('foo', 'content-encoding')) + request = DummyRequest() + request.headers = {'foo': 'bars', 'content-length': '42'} + self.assertFalse(predicates[0](Dummy(), request)) + + def test_header_multiple_with_values(self): + _, predicates, _ = self._callFUT(header=('foo:bar', 'spam:egg')) + request = DummyRequest() + request.headers = {'foo': 'bars', 'spam': 'eggs'} + self.assertTrue(predicates[0](Dummy(), request)) + + def test_header_multiple_with_values_fails(self): + _, predicates, _ = self._callFUT(header=('foo:bar', 'spam:egg$')) + request = DummyRequest() + request.headers = {'foo': 'bars', 'spam': 'eggs'} + self.assertFalse(predicates[0](Dummy(), request)) + + def test_header_multiple_mixed(self): + _, predicates, _ = self._callFUT(header=('foo:bar', 'spam')) + request = DummyRequest() + request.headers = {'foo': 'bars', 'spam': 'ham'} + self.assertTrue(predicates[0](Dummy(), request)) + + def test_header_multiple_mixed_fails(self): + _, predicates, _ = self._callFUT(header=('foo:bar', 'spam')) + request = DummyRequest() + request.headers = {'foo': 'nobar', 'spamme': 'ham'} + self.assertFalse(predicates[0](Dummy(), request)) + def test_unknown_predicate(self): from pyramid.exceptions import ConfigurationError @@ -486,6 +546,7 @@ class DummyRequest: environ = {} self.environ = environ self.params = {} + self.headers = {} self.cookies = {} |
