Merge pull request #336 from xiaclo/patch-2

Store blank parameters as '' instead of None
This commit is contained in:
Kurt Griffiths
2014-10-29 11:16:24 -05:00
5 changed files with 154 additions and 21 deletions

View File

@@ -15,7 +15,7 @@
import re
from falcon import api_helpers as helpers
from falcon.request import Request
from falcon.request import Request, RequestOptions
from falcon.response import Response
import falcon.responders
from falcon.status_codes import HTTP_416
@@ -52,8 +52,8 @@ class API(object):
"""
__slots__ = ('_after', '_before', '_request_type', '_response_type',
'_error_handlers', '_media_type',
'_routes', '_sinks', '_serialize_error')
'_error_handlers', '_media_type', '_routes', '_sinks',
'_serialize_error', 'req_options')
def __init__(self, media_type=DEFAULT_MEDIA_TYPE, before=None, after=None,
request_type=Request, response_type=Response):
@@ -69,6 +69,7 @@ class API(object):
self._error_handlers = []
self._serialize_error = helpers.serialize_error
self.req_options = RequestOptions()
def __call__(self, env, start_response):
"""WSGI `app` method.
@@ -86,7 +87,7 @@ class API(object):
"""
req = self._request_type(env)
req = self._request_type(env, options=self.req_options)
resp = self._response_type()
resource = None

View File

@@ -52,6 +52,7 @@ class Request(object):
Args:
env (dict): A WSGI environment dict passed in from the server. See
also the PEP-3333 spec.
options (dict): Set of global options passed from the API handler.
Attributes:
protocol (str): Either 'http' or 'https'.
@@ -157,6 +158,8 @@ class Request(object):
values. Where the parameter appears multiple times in the query
string, the value mapped to that parameter key will be a list of
all the values in the order seen.
options (dict): Set of global options passed from the API handler.
"""
__slots__ = (
@@ -172,13 +175,15 @@ class Request(object):
'stream',
'context',
'_wsgierrors',
'options',
)
# Allow child classes to override this
context_type = None
def __init__(self, env):
def __init__(self, env, options=None):
self.env = env
self.options = options if options else RequestOptions()
if self.context_type is None:
# Literal syntax is more efficient than using dict()
@@ -210,7 +215,10 @@ class Request(object):
if query_str:
self.query_string = uri.decode(query_str)
self._params = uri.parse_query_string(self.query_string)
self._params = uri.parse_query_string(
self.query_string,
keep_blank_qs_values=self.options.keep_blank_qs_values,
)
else:
self.query_string = six.text_type()
@@ -664,7 +672,8 @@ class Request(object):
raise HTTPMissingParam(name)
def get_param_as_bool(self, name, required=False, store=None):
def get_param_as_bool(self, name, required=False, store=None,
blank_as_true=False):
"""Return the value of a query string parameter as a boolean
The following bool-like strings are supported::
@@ -680,6 +689,9 @@ class Request(object):
store (dict, optional): A dict-like object in which to place the
value of the param, but only if the param is found (default
*None*).
blank_as_true (bool): If True, empty strings will be treated as
True. keep_blank_qs_values must be set on the Request (or API
object and inherited) for empty strings to not be filtered.
Returns:
bool: The value of the param if it is found and can be converted
@@ -705,6 +717,8 @@ class Request(object):
val = True
elif val in FALSE_STRINGS:
val = False
elif blank_as_true and not val:
val = True
else:
msg = 'The value of the parameter must be "true" or "false".'
raise HTTPInvalidParam(msg, name)
@@ -857,5 +871,22 @@ class Request(object):
'will be ignored.')
if body:
extra_params = uri.parse_query_string(uri.decode(body))
extra_params = uri.parse_query_string(
uri.decode(body),
keep_blank_qs_values=self.options.keep_blank_qs_values,
)
self._params.update(extra_params)
class RequestOptions(object):
"""This class is a container for Request options.
PERF: To avoid typos and improve storage space and speed over a dict.
"""
__slots__ = (
'keep_blank_qs_values',
)
def __init__(self):
self.keep_blank_qs_values = False

View File

@@ -245,12 +245,13 @@ else: # pragma: no cover
return decoded_uri.decode('utf-8', 'replace')
def parse_query_string(query_string):
def parse_query_string(query_string, keep_blank_qs_values=False):
"""Parse a query string into a dict.
Query string parameters are assumed to use standard form-encoding. Only
parameters with values are parsed. for example, given "foo=bar&flag",
this function would ignore "flag".
this function would ignore "flag" unless the keep_blank_qs_values option
is set.
Note:
In addition to the standard HTML form-based method for specifying
@@ -263,6 +264,8 @@ def parse_query_string(query_string):
Args:
query_string (str): The query string to parse
keep_blank_qs_values (bool): If set to True, preserves boolean fields
and fields with no content as blank strings.
Returns:
dict: A dict containing ``(name, value)`` tuples, one per query
@@ -281,7 +284,7 @@ def parse_query_string(query_string):
# and on PyPy 2.3.
for field in query_string.split('&'):
k, _, v = field.partition('=')
if not v:
if not (v or keep_blank_qs_values):
continue
if k in params:
@@ -302,10 +305,11 @@ def parse_query_string(query_string):
# point.
v = v.split(',')
# NOTE(kgriffs): Normalize the result in the case that
# some elements are empty strings, such that the result
# will be the same for 'foo=1,,3' as 'foo=1&foo=&foo=3'.
v = [element for element in v if element]
if not keep_blank_qs_values:
# NOTE(kgriffs): Normalize the result in the case that
# some elements are empty strings, such that the result
# will be the same for 'foo=1,,3' as 'foo=1&foo=&foo=3'.
v = [element for element in v if element]
params[k] = v

21
tests/test_options.py Normal file
View File

@@ -0,0 +1,21 @@
from falcon.request import RequestOptions
import falcon.testing as testing
class TestRequestOptions(testing.TestBase):
def test_correct_options(self):
options = RequestOptions()
self.assertFalse(options.keep_blank_qs_values)
options.keep_blank_qs_values = True
self.assertTrue(options.keep_blank_qs_values)
options.keep_blank_qs_values = False
self.assertFalse(options.keep_blank_qs_values)
def test_incorrect_options(self):
options = RequestOptions()
def _assign_invalid():
options.invalid_option_and_attribute = True
self.assertRaises(AttributeError, _assign_invalid)

View File

@@ -188,7 +188,7 @@ class _TestQueryParams(testing.TestBase):
def test_boolean(self):
query_string = ('echo=true&doit=false&bogus=0&bogus2=1&'
't1=True&f1=False&t2=yes&f2=no')
't1=True&f1=False&t2=yes&f2=no&blank')
self.simulate_request('/', query_string=query_string)
req = self.resource.req
@@ -212,11 +212,31 @@ class _TestQueryParams(testing.TestBase):
self.assertEqual(req.get_param_as_bool('t2'), True)
self.assertEqual(req.get_param_as_bool('f1'), False)
self.assertEqual(req.get_param_as_bool('f2'), False)
self.assertEqual(req.get_param('blank'), None)
store = {}
self.assertEqual(req.get_param_as_bool('echo', store=store), True)
self.assertEqual(store['echo'], True)
def test_boolean_blank(self):
self.api.req_options.keep_blank_qs_values = True
self.simulate_request(
'/',
query_string='blank&blank2=',
)
req = self.resource.req
self.assertEqual(req.get_param('blank'), '')
self.assertEqual(req.get_param('blank2'), '')
self.assertRaises(falcon.HTTPInvalidParam, req.get_param_as_bool,
'blank')
self.assertRaises(falcon.HTTPInvalidParam, req.get_param_as_bool,
'blank2')
self.assertEqual(req.get_param_as_bool('blank', blank_as_true=True),
True)
self.assertEqual(req.get_param_as_bool('blank3', blank_as_true=True),
None)
def test_list_type(self):
query_string = ('colors=red,green,blue&limit=1'
'&list-ish1=f,,x&list-ish2=,0&list-ish3=a,,,b'
@@ -261,6 +281,62 @@ class _TestQueryParams(testing.TestBase):
self.assertEqual(req.get_param_as_list('limit', store=store), ['1'])
self.assertEqual(store['limit'], ['1'])
def test_list_type_blank(self):
query_string = ('colors=red,green,blue&limit=1'
'&list-ish1=f,,x&list-ish2=,0&list-ish3=a,,,b'
'&empty1=&empty2=,&empty3=,,'
'&thing_one=1,,3'
'&thing_two=1&thing_two=&thing_two=3'
'&empty4=&empty4&empty4='
'&empty5&empty5&empty5')
self.api.req_options.keep_blank_qs_values = True
self.simulate_request(
'/',
query_string=query_string
)
req = self.resource.req
# 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'])
self.assertIs(req.get_param_as_list('marker'), None)
self.assertEqual(req.get_param_as_list('empty1'), [''])
self.assertEqual(req.get_param_as_list('empty2'), ['', ''])
self.assertEqual(req.get_param_as_list('empty3'), ['', '', ''])
self.assertEqual(req.get_param_as_list('list-ish1'),
['f', '', 'x'])
# Ensure that '0' doesn't get translated to None
self.assertEqual(req.get_param_as_list('list-ish2'),
['', '0'])
# Ensure that '0' doesn't get translated to None
self.assertEqual(req.get_param_as_list('list-ish3'),
['a', '', '', 'b'])
# Ensure consistency between list conventions
self.assertEqual(req.get_param_as_list('thing_one'),
['1', '', '3'])
self.assertEqual(req.get_param_as_list('thing_one'),
req.get_param_as_list('thing_two'))
store = {}
self.assertEqual(req.get_param_as_list('limit', store=store), ['1'])
self.assertEqual(store['limit'], ['1'])
# Test empty elements
self.assertEqual(req.get_param_as_list('empty4'), ['', '', ''])
self.assertEqual(req.get_param_as_list('empty5'), ['', '', ''])
self.assertEqual(req.get_param_as_list('empty4'),
req.get_param_as_list('empty5'))
def test_list_transformer(self):
query_string = 'coord=1.4,13,15.1&limit=100&things=4,,1'
self.simulate_request('/', query_string=query_string)
@@ -340,10 +416,10 @@ class _TestQueryParams(testing.TestBase):
class PostQueryParams(_TestQueryParams):
def simulate_request(self, path, query_string):
def simulate_request(self, path, query_string, **kwargs):
headers = {"Content-Type": "application/x-www-form-urlencoded"}
super(PostQueryParams, self).simulate_request(path, body=query_string,
headers=headers)
super(PostQueryParams, self).simulate_request(
path, body=query_string, headers=headers, **kwargs)
def test_non_ascii(self):
value = u'\u8c46\u74e3'
@@ -355,6 +431,6 @@ class PostQueryParams(_TestQueryParams):
class GetQueryParams(_TestQueryParams):
def simulate_request(self, path, query_string):
def simulate_request(self, path, query_string, **kwargs):
super(GetQueryParams, self).simulate_request(
path, query_string=query_string)
path, query_string=query_string, **kwargs)