Repository: falconry/falcon

Issue:
Fold repeated query param fields into a single list
Forms can be posted with multiple values for the same key, but in unpatched falcon, the last one will win. E.g. ant=1&ant=2&ant=3



Discussion:
So how to handle with the current query string? E.g. ant=1,2,3
Will it be deprecated or both are supported? 

IMO, i prefer deprecated the current query string.

See also #307


Resolving patch:
diff --git a/falcon/request.py b/falcon/request.py
--- a/falcon/request.py
+++ b/falcon/request.py
@@ -495,11 +495,16 @@ def get_param(self, name, required=False, store=None):
             parameters from the request body will be merged into
             the query string parameters.
 
-        Note:
             If a key appears more than once in the form data, one of the
             values will be returned as a string, but it is undefined which
             one.  Use .get_param_as_list() to retrieve all the values.
 
+        Note:
+            If a query parameter is assigned a comma-separated list of
+            values (e.g., foo=a,b,c) then only one of the values will be
+            returned, and it is undefined which one. Use
+            .get_param_as_list() to retrieve all the values.
+
         Args:
             name (str): Parameter name, case-sensitive (e.g., 'sort')
             required (bool, optional): Set to True to raise HTTPBadRequest
@@ -581,6 +586,7 @@ def get_param_as_int(self, name,
             val = params[name]
             if isinstance(val, list):
                 val = val[-1]
+
             try:
                 val = int(val)
             except ValueError:
@@ -642,6 +648,7 @@ def get_param_as_bool(self, name, required=False, store=None):
             val = params[name]
             if isinstance(val, list):
                 val = val[-1]
+
             if val in TRUE_STRINGS:
                 val = True
             elif val in FALSE_STRINGS:
@@ -665,7 +672,9 @@ def get_param_as_list(self, name,
                           transform=None, required=False, store=None):
         """Return the value of a query string parameter as a list.
 
-        Note that list items must be comma-separated.
+        List items must be comma-separated or must be provided
+        as multiple instances of the same param in the query string
+        ala *application/x-www-form-urlencoded*.
 
         Args:
             name (str): Parameter name, case-sensitive (e.g., 'limit')
@@ -710,13 +719,13 @@ def get_param_as_list(self, name,
         # PERF: Use if..in since it is a good all-around performer; we don't
         #       know how likely params are to be specified by clients.
         if name in params:
-            param = params[name]
-            # NOTE(warsaw): when a key appears multiple times in the request
+            items = params[name]
+
+            # NOTE(warsaw): When a key appears multiple times in the request
             # query, it will already be represented internally as a list.
-            if isinstance(param, list):
-                items = param
-            else:
-                items = param.split(',')
+            # NOTE(kgriffs): Likewise for comma-delimited values.
+            if not isinstance(items, list):
+                items = [items]
 
             # PERF(kgriffs): Use if-else rather than a DRY approach
             # that sets transform to a passthrough function; avoids
diff --git a/falcon/util/uri.py b/falcon/util/uri.py
--- a/falcon/util/uri.py
+++ b/falcon/util/uri.py
@@ -259,6 +259,15 @@ def parse_query_string(query_string):
     parameters with values are parsed. for example, given "foo=bar&flag",
     this function would ignore "flag".
 
+    Note:
+        In addition to the standard HTML form-based method for specifying
+        lists by repeating a given param multiple times, Falcon supports
+        a more compact form in which the param may be given a single time
+        but set to a list of comma-separated elements (e.g., 'foo=a,b,c').
+
+        The two different ways of specifying lists may not be mixed in
+        a single query string for the same parameter.
+
     Args:
         query_string (str): The query string to parse
 
@@ -284,7 +293,16 @@ def parse_query_string(query_string):
                 old_value.append(v)
             else:
                 params[k] = [old_value, v]
+
         else:
+            if ',' in v:
+                # NOTE(kgriffs): Falcon supports a more compact form of
+                # lists, in which the elements are comma-separated and
+                # assigned to a single param instance. If it turns out that
+                # very few people use this, it can be deprecated at some
+                # point.
+                v = v.split(',')
+
             params[k] = v
 
     return params


Test patch:
diff --git a/falcon/testing/base.py b/falcon/testing/base.py
--- a/falcon/testing/base.py
+++ b/falcon/testing/base.py
@@ -68,6 +68,12 @@ def tearDown(self):
 
         super(TestBase, self).tearDown()
 
+    # NOTE(warsaw): Pythons earlier than 2.7 do not have a self.assertIn()
+    # method, so use this compatibility function instead.
+    if not hasattr(unittest.TestCase, 'assertIn'):
+        def assertIn(self, a, b):
+            self.assertTrue(a in b)
+
     def simulate_request(self, path, decode=None, **kwargs):
         """Simulates a request to `self.api`.
 
diff --git a/tests/test_query_params.py b/tests/test_query_params.py
--- a/tests/test_query_params.py
+++ b/tests/test_query_params.py
@@ -50,7 +50,11 @@ def test_percent_encoded(self):
         self.simulate_request('/', query_string=query_string)
 
         req = self.resource.req
-        self.assertEqual(req.get_param('id'), u'23,42')
+
+        # NOTE(kgriffs): For lists, get_param will return one of the
+        # elements, but which one it will choose is undefined.
+        self.assertIn(req.get_param('id'), [u'23', u'42'])
+
         self.assertEqual(req.get_param_as_list('id', int), [23, 42])
         self.assertEqual(req.get_param('q'), u'\u8c46 \u74e3')
 
@@ -199,7 +203,11 @@ def test_list_type(self):
         self.simulate_request('/', query_string=query_string)
 
         req = self.resource.req
-        self.assertEqual(req.get_param('colors'), 'red,green,blue')
+
+        # NOTE(kgriffs): For lists, get_param will return one of the
+        # elements, but which one it will choose is undefined.
+        self.assertIn(req.get_param('colors'), ('red', 'green', 'blue'))
+
         self.assertEqual(req.get_param_as_list('colors'),
                          ['red', 'green', 'blue'])
         self.assertEqual(req.get_param_as_list('limit'), ['1'])
@@ -229,7 +237,10 @@ def test_list_transformer(self):
         self.simulate_request('/', query_string=query_string)
 
         req = self.resource.req
-        self.assertEqual(req.get_param('coord'), '1.4,13,15.1')
+
+        # NOTE(kgriffs): For lists, get_param will return one of the
+        # elements, but which one it will choose is undefined.
+        self.assertIn(req.get_param('coord'), ('1.4', '13', '15.1'))
 
         expected = [1.4, 13.0, 15.1]
         actual = req.get_param_as_list('coord', transform=float)
@@ -255,12 +266,6 @@ def test_param_property(self):
             sorted(req.params.items()),
             [('ant', '4'), ('bee', '3'), ('cat', '2'), ('dog', '1')])
 
-    # NOTE(warsaw): Pythons earlier than 2.7 do not have a self.assertIn()
-    # method, so use this compatibility function instead.
-    if not hasattr(testing.TestBase, 'assertIn'):
-        def assertIn(self, a, b):
-            self.assertTrue(a in b)
-
     def test_multiple_form_keys(self):
         query_string = 'ant=1&ant=2&bee=3&cat=6&cat=5&cat=4'
         self.simulate_request('/', query_string=query_string)