Repository: Pylons/pyramid

Issue:
request_param in @view_config should support tuples
request_param should support tuples just as match_param does.  Among other things, this would be very useful for oauth where multiple parameters are required to be present for a valid request.



Discussion:
I'm tackling this one, but I can't assign myself.  Go ahead and assign to me if someone wants to.

Here's the context mcdonc gave me in #pyramid:

> mcdonc_: it's what happens when you do
> mcdonc_: config.add_view(request_param='foo=bar')
> mcdonc_: desirable to be able to do
> mcdonc_: config.add_view(request_param=('foo=bar', 'biz=baz'))
> mcdonc_: the code is in pyramid/config/views.py and likely pyramid/config/util.py
> mcdonc_: all the zopey stuff is in pyramid/config.. discriminators and conflict resolution and so on

I've got a very preliminary attempt at generalizing the `match_param` handling and using it for both that and `request_param`:

https://github.com/rpatterson/pyramid

The existing tests and two new tests I just copied and adjusted very naively  are all passing but I really need to learn what this stuff is for and write more comprehensive tests.  But for now I need to switch to http://bugs.repoze.org/issue177


Resolving patch:
diff --git a/pyramid/config/predicates.py b/pyramid/config/predicates.py
--- a/pyramid/config/predicates.py
+++ b/pyramid/config/predicates.py
@@ -64,43 +64,48 @@ def __call__(self, context, request):
     
 class RequestParamPredicate(object):
     def __init__(self, val, config):
-        name = val
-        v = None
-        if '=' in name:
-            name, v = name.split('=', 1)
-            name, v = name.strip(), v.strip()
-        if v is None:
-            self._text = 'request_param %s' % (name,)
-        else:
-            self._text = 'request_param %s = %s' % (name, v)
-        self.name = name
-        self.val = v
+        val = as_sorted_tuple(val)
+        reqs = []
+        for p in val:
+            k = p
+            v = None
+            if '=' in p:
+                k, v = p.split('=', 1)
+                k, v = k.strip(), v.strip()
+            reqs.append((k, v))
+        self.val = val
+        self.reqs = reqs
 
     def text(self):
-        return self._text
+        return 'request_param %s' % ','.join(
+            ['%s=%s' % (x,y) if y else x for x, y in self.reqs]
+        )
 
     phash = text
 
     def __call__(self, context, request):
-        if self.val is None:
-            return self.name in request.params
-        return request.params.get(self.name) == self.val
-    
+        for k, v in self.reqs:
+            actual = request.params.get(k)
+            if actual is None:
+                return False
+            if v is not None and actual != v:
+                return False
+        return True
 
 class HeaderPredicate(object):
     def __init__(self, val, config):
         name = val
         v = None
         if ':' in name:
-            name, v = name.split(':', 1)
+            name, val_str = name.split(':', 1)
             try:
-                v = re.compile(v)
+                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, v)
+            self._text = 'header %s=%s' % (name, val_str)
         self.name = name
         self.val = v
 
@@ -156,9 +161,7 @@ def __call__(self, context, request):
     
 class MatchParamPredicate(object):
     def __init__(self, val, config):
-        if not is_nonstr_iter(val):
-            val = (val,)
-        val = sorted(val)
+        val = as_sorted_tuple(val)
         self.val = val
         reqs = [ p.split('=', 1) for p in val ]
         self.reqs = [ (x.strip(), y.strip()) for x, y in reqs ]


Test patch:
diff --git a/pyramid/tests/test_config/test_predicates.py b/pyramid/tests/test_config/test_predicates.py
--- a/pyramid/tests/test_config/test_predicates.py
+++ b/pyramid/tests/test_config/test_predicates.py
@@ -117,6 +117,20 @@ def test___call___true_withval(self):
         result = inst(None, request)
         self.assertTrue(result)
 
+    def test___call___true_multi(self):
+        inst = self._makeOne(('abc', 'def =2 '))
+        request = Dummy()
+        request.params = {'abc':'1', 'def': '2'}
+        result = inst(None, request)
+        self.assertTrue(result)
+
+    def test___call___false_multi(self):
+        inst = self._makeOne(('abc=3', 'def =2 '))
+        request = Dummy()
+        request.params = {'abc':'3', 'def': '1'}
+        result = inst(None, request)
+        self.assertFalse(result)
+
     def test___call___false(self):
         inst = self._makeOne('abc')
         request = Dummy()
@@ -130,7 +144,11 @@ def test_text_exists(self):
 
     def test_text_withval(self):
         inst = self._makeOne('abc=  1')
-        self.assertEqual(inst.text(), 'request_param abc = 1')
+        self.assertEqual(inst.text(), 'request_param abc=1')
+
+    def test_text_multi(self):
+        inst = self._makeOne(('abc=  1', 'def'))
+        self.assertEqual(inst.text(), 'request_param abc=1,def')
 
     def test_phash_exists(self):
         inst = self._makeOne('abc')
@@ -138,7 +156,7 @@ def test_phash_exists(self):
 
     def test_phash_withval(self):
         inst = self._makeOne('abc=   1')
-        self.assertEqual(inst.phash(), "request_param abc = 1")
+        self.assertEqual(inst.phash(), "request_param abc=1")
 
 class TestMatchParamPredicate(unittest.TestCase):
     def _makeOne(self, val):
@@ -299,6 +317,70 @@ def test_it_call_val_False(self):
         result = inst(None, request)
         self.assertEqual(result, True)
 
+class TestHeaderPredicate(unittest.TestCase):
+    def _makeOne(self, val):
+        from pyramid.config.predicates import HeaderPredicate
+        return HeaderPredicate(val, None)
+
+    def test___call___true_exists(self):
+        inst = self._makeOne('abc')
+        request = Dummy()
+        request.headers = {'abc':1}
+        result = inst(None, request)
+        self.assertTrue(result)
+
+    def test___call___true_withval(self):
+        inst = self._makeOne('abc:1')
+        request = Dummy()
+        request.headers = {'abc':'1'}
+        result = inst(None, request)
+        self.assertTrue(result)
+
+    def test___call___true_withregex(self):
+        inst = self._makeOne(r'abc:\d+')
+        request = Dummy()
+        request.headers = {'abc':'1'}
+        result = inst(None, request)
+        self.assertTrue(result)
+
+    def test___call___false_withregex(self):
+        inst = self._makeOne(r'abc:\d+')
+        request = Dummy()
+        request.headers = {'abc':'a'}
+        result = inst(None, request)
+        self.assertFalse(result)
+
+    def test___call___false(self):
+        inst = self._makeOne('abc')
+        request = Dummy()
+        request.headers = {}
+        result = inst(None, request)
+        self.assertFalse(result)
+
+    def test_text_exists(self):
+        inst = self._makeOne('abc')
+        self.assertEqual(inst.text(), 'header abc')
+
+    def test_text_withval(self):
+        inst = self._makeOne('abc:1')
+        self.assertEqual(inst.text(), 'header abc=1')
+
+    def test_text_withregex(self):
+        inst = self._makeOne(r'abc:\d+')
+        self.assertEqual(inst.text(), r'header abc=\d+')
+
+    def test_phash_exists(self):
+        inst = self._makeOne('abc')
+        self.assertEqual(inst.phash(), 'header abc')
+
+    def test_phash_withval(self):
+        inst = self._makeOne('abc:1')
+        self.assertEqual(inst.phash(), "header abc=1")
+
+    def test_phash_withregex(self):
+        inst = self._makeOne(r'abc:\d+')
+        self.assertEqual(inst.phash(), r'header abc=\d+')
+
 class predicate(object):
     def __repr__(self):
         return 'predicate'