Repository: falconry/falcon

Issue:
request.get_param_as_list returns non-empty list for empty value
If the value of the param is an empty string, an empty list should be returned, not ['']



Discussion:
At a first glance, `""` should be an empty list; however, if you think it's correct for `","` to yield `['', '']`, then how to represent a list with one empty string?


Resolving patch:
diff --git a/falcon/request.py b/falcon/request.py
--- a/falcon/request.py
+++ b/falcon/request.py
@@ -549,7 +549,9 @@ def get_param_as_list(self, name,
             name: Parameter name, case-sensitive (e.g., 'limit')
             transform: An optional transform function that takes as input
                 each element in the list as a string and outputs a transformed
-                element for inclusion in the list that will be returned.
+                element for inclusion in the list that will be returned. For
+                example, passing the int function will transform list items
+                into numbers.
             required: Set to True to raise HTTPBadRequest instead of returning
                 gracefully when the parameter is not found or is not an
                 integer (default False)
@@ -558,21 +560,43 @@ def get_param_as_list(self, name,
 
         Returns:
             The value of the param if it is found. Otherwise, returns None
-            unless required is True.
+            unless required is True. for partial lists, None will be returned
+            as a placeholder. For example:
+
+                things=1,,3
+
+            would be returned as:
+
+                ['1', None, '3']
+
+            while this:
+
+                things=,,,
+
+            would just be retured as:
+
+                [None, None, None, None]
 
         Raises
             HTTPBadRequest: The param was not found in the request, but was
                 required.
-
         """
 
         # 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 self._params:
             items = self._params[name].split(',')
-            if transform is not None:
+
+            # PERF(kgriffs): Use if-else rather than a DRY approach
+            # that sets transform to a passthrough function; avoids
+            # function calling overhead.
+            if transform is None:
+                items = [i if i != '' else None
+                         for i in items]
+            else:
                 try:
-                    items = [transform(x) for x in items]
+                    items = [transform(i) if i != '' else None
+                             for i in items]
                 except ValueError:
                     desc = ('The value of the "' + name + '" query parameter '
                             'is not formatted correctly.')


Test patch:
diff --git a/falcon/tests/test_query_params.py b/falcon/tests/test_query_params.py
--- a/falcon/tests/test_query_params.py
+++ b/falcon/tests/test_query_params.py
@@ -181,7 +181,9 @@ def test_boolean(self):
         self.assertEquals(store['echo'], True)
 
     def test_list_type(self):
-        query_string = 'colors=red,green,blue&limit=1'
+        query_string = ('colors=red,green,blue&limit=1'
+                        '&list-ish1=f,,x&list-ish2=,0&list-ish3=a,,,b'
+                        '&empty1=&empty2=,&empty3=,,')
         self.simulate_request('/', query_string=query_string)
 
         req = self.resource.req
@@ -191,12 +193,27 @@ def test_list_type(self):
         self.assertEquals(req.get_param_as_list('limit'), ['1'])
         self.assertIs(req.get_param_as_list('marker'), None)
 
+        self.assertEquals(req.get_param_as_list('empty1'), None)
+        self.assertEquals(req.get_param_as_list('empty2'), [None, None])
+        self.assertEquals(req.get_param_as_list('empty3'), [None, None, None])
+
+        self.assertEquals(req.get_param_as_list('list-ish1'),
+                          ['f', None, 'x'])
+
+        # Ensure that '0' doesn't get translated to None
+        self.assertEquals(req.get_param_as_list('list-ish2'),
+                          [None, '0'])
+
+        # Ensure that '0' doesn't get translated to None
+        self.assertEquals(req.get_param_as_list('list-ish3'),
+                          ['a', None, None, 'b'])
+
         store = {}
         self.assertEquals(req.get_param_as_list('limit', store=store), ['1'])
         self.assertEquals(store['limit'], ['1'])
 
     def test_list_transformer(self):
-        query_string = 'coord=1.4,13,15.1&limit=100'
+        query_string = 'coord=1.4,13,15.1&limit=100&things=4,,1'
         self.simulate_request('/', query_string=query_string)
 
         req = self.resource.req
@@ -206,15 +223,13 @@ def test_list_transformer(self):
         actual = req.get_param_as_list('coord', transform=float)
         self.assertEquals(actual, expected)
 
-        self.assertRaises(falcon.HTTPBadRequest,
-                          req.get_param_as_list, 'coord', transform=int)
+        expected = ['4', None, '1']
+        actual = req.get_param_as_list('things', transform=str)
+        self.assertEquals(actual, expected)
 
-    def test_bogus_input(self):
-        query_string = 'colors=red,green,&limit=1&pickle'
-        self.simulate_request('/', query_string=query_string)
+        expected = [4, None, 1]
+        actual = req.get_param_as_list('things', transform=int)
+        self.assertEquals(actual, expected)
 
-        req = self.resource.req
-        self.assertEquals(req.get_param_as_list('colors'),
-                          ['red', 'green', ''])
-        self.assertEquals(req.get_param('limit'), '1')
-        self.assertIs(req.get_param('pickle'), None)
+        self.assertRaises(falcon.HTTPBadRequest,
+                          req.get_param_as_list, 'coord', transform=int)