diff options
| -rw-r--r-- | CHANGES.rst | 4 | ||||
| -rw-r--r-- | docs/narr/viewconfig.rst | 39 | ||||
| -rw-r--r-- | src/pyramid/config/routes.py | 39 | ||||
| -rw-r--r-- | src/pyramid/config/views.py | 34 | ||||
| -rw-r--r-- | src/pyramid/predicates.py | 52 | ||||
| -rw-r--r-- | tests/test_config/test_predicates.py | 81 |
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 = {} |
