summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.rst4
-rw-r--r--docs/narr/viewconfig.rst39
-rw-r--r--src/pyramid/config/routes.py39
-rw-r--r--src/pyramid/config/views.py34
-rw-r--r--src/pyramid/predicates.py52
-rw-r--r--tests/test_config/test_predicates.py81
6 files changed, 173 insertions, 76 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..b43ebb93e 100644
--- a/docs/narr/viewconfig.rst
+++ b/docs/narr/viewconfig.rst
@@ -391,7 +391,7 @@ configured view.
the ``REQUEST_METHOD`` of the :term:`WSGI` environment.
``request_param``
- This value can be any string or a sequence of strings. A view declaration
+ This argument can be any string or a sequence of strings. A view declaration
with this argument ensures that the view will only be called when the
:term:`request` has a key in the ``request.params`` dictionary (an HTTP
``GET`` or ``POST`` variable) that has a name which matches the supplied
@@ -406,7 +406,7 @@ configured view.
consideration of keys and values in the ``request.params`` dictionary.
``match_param``
- This param may be either a single string of the format "key=value" or a tuple
+ This argument may be either a single string of the format "key=value" or a tuple
containing one or more of these strings.
This argument ensures that the view will only be called when the
@@ -448,24 +448,23 @@ configured view.
associated view callable.
``header``
- This value represents an HTTP header name or a header name/value pair.
-
- If ``header`` is specified, it must be 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
- exists with any value in the request.
-
- If ``header`` is specified, and possesses a name/value pair (e.g.,
- ``User-Agent:Mozilla/.*``), the view will only be invoked if the HTTP header
- exists *and* the HTTP header matches the value requested. When the
- ``headervalue`` 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.
-
- Whether or not the value represents a header name or a header name/value
- pair, the case of the header name is not significant.
+ This param matches one or more HTTP header names or header name/value pairs.
+ If specified, this param must be a string or a sequence of strings,
+ each string being a header name or a ``headername:headervalue`` pair.
+
+ - Each string specified as a bare header name without a value (for example
+ ``If-Modified-Since``) will match a request if it contains an HTTP header
+ with that same name. The case of the name is not significant, and the
+ header may have any value in the request.
+
+ - Each string specified as a name/value pair (that is, if it contains a ``:``
+ (colon), like ``User-Agent:Mozilla/.*``) will match a request only if it
+ contains an HTTP header with the requested name (ignoring case, so
+ ``User-Agent`` or ``user-agent`` would both match), *and* the value of the
+ HTTP header matches the value requested (``Mozilla/.*`` in our example).
+ The value portion is interpreted as a regular expression.
+
+ The view will only be invoked if all strings are matching.
If ``header`` is not specified, the composition, presence, or absence of HTTP
headers is not taken into consideration when deciding whether or not to
diff --git a/src/pyramid/config/routes.py b/src/pyramid/config/routes.py
index 9452a05ab..a12e18fa8 100644
--- a/src/pyramid/config/routes.py
+++ b/src/pyramid/config/routes.py
@@ -211,31 +211,32 @@ class RoutesConfiguratorMixin:
dictionary (an HTTP ``GET`` or ``POST`` variable) that has a
name which matches the supplied value. If the value
supplied as the argument has a ``=`` sign in it,
- e.g. ``request_param="foo=123"``, then the key
- (``foo``) must both exist in the ``request.params`` dictionary, and
+ e.g. ``request_param="foo=123"``, then both the key
+ (``foo``) must exist in the ``request.params`` dictionary, and
the value must match the right hand side of the expression (``123``)
for the route to "match" the current request. If this predicate
returns ``False``, route matching continues.
header
- This argument represents an HTTP header name or a header
- name/value pair. 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
- regular expression. If the value does not contain a colon,
- the entire value will be considered to be the header name
- (e.g. ``If-Modified-Since``). If the value evaluates to a
- header name only without a value, the header specified by
- the name must be present in the request for this predicate
- to be true. If the value evaluates to a header name/value
- pair, the header specified by the name must be present in
- the request *and* the regular expression specified as the
- value must match the header value. Whether or not the value
- represents a header name or a header name/value pair, the
- case of the header name is not significant. If this
- predicate returns ``False``, route matching continues.
+ This argument can be a string or an iterable of strings for HTTP
+ headers. The matching is determined as follow:
+
+ - If a string does not contain a ``:`` (colon), it will be
+ considered to be the header name (example ``If-Modified-Since``).
+ In this case, the header specified by the name must be present
+ in the request for this string to match. Case is not significant.
+
+ - If a string contains a colon, it will be considered a
+ name/value pair (for example ``User-Agent:Mozilla/.*`` or
+ ``Host:localhost``), where the value part is a regular
+ expression. The header specified by the name must be present
+ in the request *and* the regular expression specified as the
+ value part must match the value of the request header. Case is
+ not significant for the header name, but it is for the value.
+
+ All strings must be matched for this predicate to return ``True``.
+ If this predicate returns ``False``, route matching continues.
accept
diff --git a/src/pyramid/config/views.py b/src/pyramid/config/views.py
index 6faa29d20..a064ebd05 100644
--- a/src/pyramid/config/views.py
+++ b/src/pyramid/config/views.py
@@ -670,22 +670,24 @@ class ViewsConfiguratorMixin:
header
- This value represents an HTTP header name or a header
- name/value pair. 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
- does not contain a colon, the entire value will be
- considered to be the header name
- (e.g. ``If-Modified-Since``). If the value evaluates to a
- header name only without a value, the header specified by
- the name must be present in the request for this predicate
- to be true. If the value evaluates to a header name/value
- pair, the header specified by the name must be present in
- the request *and* the regular expression specified as the
- value must match the header value. Whether or not the value
- represents a header name or a header name/value pair, the
- case of the header name is not significant.
+ This argument can be a string or an iterable of strings for HTTP
+ headers. The matching is determined as follow:
+
+ - If a string does not contain a ``:`` (colon), it will be
+ considered to be a header name (example ``If-Modified-Since``).
+ In this case, the header specified by the name must be present
+ in the request for this string to match. Case is not significant.
+
+ - If a string contains a colon, it will be considered a
+ name/value pair (for example ``User-Agent:Mozilla/.*`` or
+ ``Host:localhost``), where the value part is a regular
+ expression. The header specified by the name must be present
+ in the request *and* the regular expression specified as the
+ value part must match the value of the request header. Case is
+ not significant for the header name, but it is for the value.
+
+ All strings must be matched for this predicate to return ``True``.
+ If this predicate returns ``False``, view matching continues.
path_info
diff --git a/src/pyramid/predicates.py b/src/pyramid/predicates.py
index 8b443e79b..576bbbce6 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 d1562947e..8017fc898 100644
--- a/tests/test_config/test_predicates.py
+++ b/tests/test_config/test_predicates.py
@@ -312,6 +312,20 @@ class TestPredicateList(unittest.TestCase):
self.assertEqual(predicates[10].text(), 'classmethod predicate')
self.assertTrue(predicates[11].text().startswith('custom predicate'))
+ def test_predicate_text_is_correct_when_multiple(self):
+ _, predicates, _ = self._callFUT(
+ request_method=('one', 'two'),
+ 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 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")
+
def test_match_param_from_string(self):
_, predicates, _ = self._callFUT(match_param='foo=bar')
request = DummyRequest()
@@ -354,6 +368,72 @@ 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_with_value_fails_case(self):
+ _, predicates, _ = self._callFUT(header='foo:bar')
+ request = DummyRequest()
+ request.headers = {'foo': 'BAR'}
+ 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
@@ -472,6 +552,7 @@ class DummyRequest:
environ = {}
self.environ = environ
self.params = {}
+ self.headers = {}
self.cookies = {}