Skip to content

Commit a33301e

Browse files
rushevichvytas7
andauthored
feat(request): add delimiter support to get_param_as_list (#2540)
* feat(request): add delimiter support to get_param_as_list Add a new delimiter keyword argument to eq.get_param_as_list to support splitting query parameter values on characters other than a comma. Closes #2538 * feat(req): address review comments --------- Co-authored-by: Vytautas Liuolia <vytautas.liuolia@gmail.com>
1 parent a54317c commit a33301e

File tree

3 files changed

+112
-1
lines changed

3 files changed

+112
-1
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
The :meth:`req.get_param_as_list <falcon.Request.get_param_as_list>` method now
2+
supports a new argument, `delimiter`, for splitting of values.
3+
In line with the OpenAPI v3 parameter specification, the supported delimiters
4+
currently include the ``'pipeDelimited'`` and ``'spaceDelimited'`` symbolic
5+
constants, as well as the literal ``','``, ``'|'``, and ``' '`` characters.

falcon/request.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@
5858
FALSE_STRINGS = frozenset(['false', 'False', 'f', 'no', 'n', '0', 'off'])
5959
WSGI_CONTENT_HEADERS = frozenset(['CONTENT_TYPE', 'CONTENT_LENGTH'])
6060

61+
_PARAM_VALUE_DELIMITERS = {
62+
',': ',',
63+
'|': '|',
64+
' ': ' ',
65+
'pipeDelimited': '|',
66+
'spaceDelimited': ' ',
67+
}
68+
6169
# PERF(kgriffs): Avoid an extra namespace lookup when using these functions
6270
strptime = datetime.strptime
6371
now = datetime.now
@@ -1944,6 +1952,7 @@ def get_param_as_list(
19441952
required: Literal[True],
19451953
store: StoreArg = ...,
19461954
default: list[str] | None = ...,
1955+
delimiter: str | None = None,
19471956
) -> list[str]: ...
19481957

19491958
@overload
@@ -1954,6 +1963,7 @@ def get_param_as_list(
19541963
required: Literal[True],
19551964
store: StoreArg = ...,
19561965
default: list[_T] | None = ...,
1966+
delimiter: str | None = None,
19571967
) -> list[_T]: ...
19581968

19591969
@overload
@@ -1965,6 +1975,7 @@ def get_param_as_list(
19651975
store: StoreArg = ...,
19661976
*,
19671977
default: list[str],
1978+
delimiter: str | None = None,
19681979
) -> list[str]: ...
19691980

19701981
@overload
@@ -1976,6 +1987,7 @@ def get_param_as_list(
19761987
store: StoreArg = ...,
19771988
*,
19781989
default: list[_T],
1990+
delimiter: str | None = None,
19791991
) -> list[_T]: ...
19801992

19811993
@overload
@@ -1986,6 +1998,7 @@ def get_param_as_list(
19861998
required: bool = ...,
19871999
store: StoreArg = ...,
19882000
default: list[str] | None = ...,
2001+
delimiter: str | None = None,
19892002
) -> list[str] | None: ...
19902003

19912004
@overload
@@ -1996,6 +2009,7 @@ def get_param_as_list(
19962009
required: bool = ...,
19972010
store: StoreArg = ...,
19982011
default: list[_T] | None = ...,
2012+
delimiter: str | None = None,
19992013
) -> list[_T] | None: ...
20002014

20012015
def get_param_as_list(
@@ -2005,6 +2019,7 @@ def get_param_as_list(
20052019
required: bool = False,
20062020
store: StoreArg = None,
20072021
default: list[_T] | None = None,
2022+
delimiter: str | None = None,
20082023
) -> list[_T] | list[str] | None:
20092024
"""Return the value of a query string parameter as a list.
20102025
@@ -2033,7 +2048,33 @@ def get_param_as_list(
20332048
the value of the param, but only if the param is found (default
20342049
``None``).
20352050
default (any): If the param is not found returns the
2036-
given value instead of ``None``
2051+
given value instead of ``None``.
2052+
delimiter(str): An optional character for splitting a parameter
2053+
value into a list. In addition to the ``','``, ``' '``, and
2054+
``'|'`` characters, the ``'spaceDelimited'`` and
2055+
``'pipeDelimited'`` symbolic constants from the
2056+
`OpenAPI v3 parameter specification
2057+
<https://spec.openapis.org/oas/v3.2.0.html#style-values>`__
2058+
are also supported.
2059+
2060+
Note:
2061+
If the parameter was already passed as an array, e.g., as
2062+
multiple instances (the OAS ``'explode'`` style), the
2063+
`delimiter` argument has no effect.
2064+
2065+
Note:
2066+
In contrast to the automatic splitting of comma-separated
2067+
values via the
2068+
:attr:`~falcon.RequestOptions.auto_parse_qs_csv` option,
2069+
values are split by `delimiter` **after** percent-decoding
2070+
the query string.
2071+
2072+
The :attr:`~falcon.RequestOptions.keep_blank_qs_values`
2073+
option has no effect on the secondary splitting by
2074+
`delimiter` either.
2075+
2076+
.. versionadded:: 4.3
2077+
The `delimiter` keyword argument.
20372078
20382079
Returns:
20392080
list: The value of the param if it is found. Otherwise, returns
@@ -2053,6 +2094,15 @@ def get_param_as_list(
20532094
:attr:`~falcon.RequestOptions.auto_parse_qs_csv` option must be
20542095
set to ``True``.
20552096
2097+
Even if the :attr:`~falcon.RequestOptions.auto_parse_qs_csv` option
2098+
is set (by default) to ``False``, a value can also be split into
2099+
list elements by using an OpenAPI spec-compatible delimiter, e.g.:
2100+
2101+
>>> req
2102+
<Request: GET 'http://falconframework.org/?colors=blue%7Cblack%7Cbrown'>
2103+
>>> req.get_param_as_list('colors', delimiter='pipeDelimited')
2104+
['blue', 'black', 'brown']
2105+
20562106
Raises:
20572107
HTTPBadRequest: A required param is missing from the request, or
20582108
a transform function raised an instance of ``ValueError``.
@@ -2066,6 +2116,16 @@ def get_param_as_list(
20662116
if name in params:
20672117
items = params[name]
20682118

2119+
# NOTE(bricklayer25): If a delimiter is specified AND the param is
2120+
# a single string, split it.
2121+
if delimiter is not None and isinstance(items, str):
2122+
if delimiter not in _PARAM_VALUE_DELIMITERS:
2123+
raise ValueError(
2124+
f'Unsupported delimiter value: {delimiter!r};'
2125+
f' supported: {tuple(_PARAM_VALUE_DELIMITERS)}'
2126+
)
2127+
items = items.split(_PARAM_VALUE_DELIMITERS[delimiter])
2128+
20692129
# NOTE(warsaw): When a key appears multiple times in the request
20702130
# query, it will already be represented internally as a list.
20712131
# NOTE(kgriffs): Likewise for comma-delimited values.

tests/test_request_attrs.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,6 +1012,52 @@ def test_etag_parsing_helper(self, asgi, header_value):
10121012

10131013
assert _parse_etags(header_value) is None
10141014

1015+
def test_get_param_as_list_comma_delimited(self, asgi):
1016+
req = create_req(asgi, query_string='names=Luke,Leia,Han')
1017+
result = req.get_param_as_list('names', delimiter=',')
1018+
assert result == ['Luke', 'Leia', 'Han']
1019+
1020+
@pytest.mark.parametrize('delimiter', [' ', 'spaceDelimited'])
1021+
def test_get_param_as_list_space_delimited(self, asgi, delimiter):
1022+
req = create_req(asgi, query_string='names=Luke%20Leia%20Han')
1023+
result = req.get_param_as_list('names', delimiter=delimiter)
1024+
assert result == ['Luke', 'Leia', 'Han']
1025+
1026+
@pytest.mark.parametrize(
1027+
'query_string', ['names=Luke|Leia|Han', 'names=Luke%7CLeia%7CHan']
1028+
)
1029+
@pytest.mark.parametrize('delimiter', ['|', 'pipeDelimited'])
1030+
def test_get_param_as_list_pipe_delimited(self, asgi, query_string, delimiter):
1031+
req = create_req(asgi, query_string=query_string)
1032+
result = req.get_param_as_list('names', delimiter=delimiter)
1033+
assert result == ['Luke', 'Leia', 'Han']
1034+
1035+
def test_get_param_as_list_unsupported_delimiter(self, asgi):
1036+
req = create_req(asgi, query_string='names=Luke;Leia;Han')
1037+
with pytest.raises(ValueError):
1038+
req.get_param_as_list('names', delimiter=';')
1039+
1040+
@pytest.mark.parametrize('delimiter', ['pipeDelimited', 'spaceDelimited'])
1041+
def test_get_param_as_list_parse_qs_csv_vs_delimiter(self, asgi, delimiter):
1042+
options = falcon.RequestOptions()
1043+
options.auto_parse_qs_csv = True
1044+
1045+
req = create_req(
1046+
asgi, query_string='names=value 1,value|2,value 3', options=options
1047+
)
1048+
1049+
result = req.get_param_as_list('names', delimiter=delimiter)
1050+
1051+
assert result == ['value 1', 'value|2', 'value 3']
1052+
1053+
@pytest.mark.parametrize('delimiter', [' ', 'spaceDelimited'])
1054+
def test_get_param_as_list_multiple_values_vs_delimiter(self, asgi, delimiter):
1055+
req = create_req(
1056+
asgi, query_string='phrase=quick%20brown%20fox&phrase=lazy%20dog'
1057+
)
1058+
result = req.get_param_as_list('phrase', delimiter=delimiter)
1059+
assert result == ['quick brown fox', 'lazy dog']
1060+
10151061
# -------------------------------------------------------------------------
10161062
# Helpers
10171063
# -------------------------------------------------------------------------

0 commit comments

Comments
 (0)