add Request.not_grouped_args, deprecation warning Request.raw_args (#1476)
* add Request.not_grouped_args, deprecation warning Request.raw_args * add 1 more test for coverage * custom parser for Request.args and Request.query_args, some additional tests * add docs for custom queryset parsing * fix import sorting * docstrings for get_query_args and get_args methods * lost import
This commit is contained in:
parent
d5813152ab
commit
2a15583b87
|
@ -19,6 +19,8 @@ The following variables are accessible as properties on `Request` objects:
|
||||||
URL that resembles `?key1=value1&key2=value2`. If that URL were to be parsed,
|
URL that resembles `?key1=value1&key2=value2`. If that URL were to be parsed,
|
||||||
the `args` dictionary would look like `{'key1': ['value1'], 'key2': ['value2']}`.
|
the `args` dictionary would look like `{'key1': ['value1'], 'key2': ['value2']}`.
|
||||||
The request's `query_string` variable holds the unparsed string value.
|
The request's `query_string` variable holds the unparsed string value.
|
||||||
|
Property is providing the default parsing strategy. If you would like to change it look to the section below
|
||||||
|
(`Changing the default parsing rules of the queryset`).
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from sanic.response import json
|
from sanic.response import json
|
||||||
|
@ -28,9 +30,54 @@ The following variables are accessible as properties on `Request` objects:
|
||||||
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
|
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
|
||||||
```
|
```
|
||||||
|
|
||||||
- `raw_args` (dict) - On many cases you would need to access the url arguments in
|
- `query_args` (list) - On many cases you would need to access the url arguments in
|
||||||
a less packed dictionary. For same previous URL `?key1=value1&key2=value2`, the
|
a less packed form. `query_args` is the list of `(key, value)` tuples.
|
||||||
`raw_args` dictionary would look like `{'key1': 'value1', 'key2': 'value2'}`.
|
Property is providing the default parsing strategy. If you would like to change it look to the section below
|
||||||
|
(`Changing the default parsing rules of the queryset`).
|
||||||
|
For the same previous URL queryset `?key1=value1&key2=value2`, the
|
||||||
|
`query_args` list would look like `[('key1', 'value1'), ('key2', 'value2')]`.
|
||||||
|
And in case of the multiple params with the same key like `?key1=value1&key2=value2&key1=value3`
|
||||||
|
the `query_args` list would look like `[('key1', 'value1'), ('key2', 'value2'), ('key1', 'value3')]`.
|
||||||
|
|
||||||
|
The difference between Request.args and Request.query_args
|
||||||
|
for the queryset `?key1=value1&key2=value2&key1=value3`
|
||||||
|
|
||||||
|
```python
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/test_request_args")
|
||||||
|
async def test_request_args(request):
|
||||||
|
return json({
|
||||||
|
"parsed": True,
|
||||||
|
"url": request.url,
|
||||||
|
"query_string": request.query_string,
|
||||||
|
"args": request.args,
|
||||||
|
"raw_args": request.raw_args,
|
||||||
|
"query_args": request.query_args,
|
||||||
|
})
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host="0.0.0.0", port=8000)
|
||||||
|
```
|
||||||
|
|
||||||
|
Output
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"parsed":true,
|
||||||
|
"url":"http:\/\/0.0.0.0:8000\/test_request_args?key1=value1&key2=value2&key1=value3",
|
||||||
|
"query_string":"key1=value1&key2=value2&key1=value3",
|
||||||
|
"args":{"key1":["value1","value3"],"key2":["value2"]},
|
||||||
|
"raw_args":{"key1":"value1","key2":"value2"},
|
||||||
|
"query_args":[["key1","value1"],["key2","value2"],["key1","value3"]]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`raw_args` contains only the first entry of `key1`. Will be deprecated in the future versions.
|
||||||
|
|
||||||
- `files` (dictionary of `File` objects) - List of files that have a name, body, and type
|
- `files` (dictionary of `File` objects) - List of files that have a name, body, and type
|
||||||
|
|
||||||
|
@ -106,6 +153,51 @@ The following variables are accessible as properties on `Request` objects:
|
||||||
- `token`: The value of Authorization header: `Basic YWRtaW46YWRtaW4=`
|
- `token`: The value of Authorization header: `Basic YWRtaW46YWRtaW4=`
|
||||||
|
|
||||||
|
|
||||||
|
## Changing the default parsing rules of the queryset
|
||||||
|
|
||||||
|
The default parameters that are using internally in `args` and `query_args` properties to parse queryset:
|
||||||
|
|
||||||
|
- `keep_blank_values` (bool): `False` - flag indicating whether blank values in
|
||||||
|
percent-encoded queries should be treated as blank strings.
|
||||||
|
A true value indicates that blanks should be retained as blank
|
||||||
|
strings. The default false value indicates that blank values
|
||||||
|
are to be ignored and treated as if they were not included.
|
||||||
|
- `strict_parsing` (bool): `False` - flag indicating what to do with parsing errors. If
|
||||||
|
false (the default), errors are silently ignored. If true,
|
||||||
|
errors raise a ValueError exception.
|
||||||
|
- `encoding` and `errors` (str): 'utf-8' and 'replace' - specify how to decode percent-encoded sequences
|
||||||
|
into Unicode characters, as accepted by the bytes.decode() method.
|
||||||
|
|
||||||
|
If you would like to change that default parameters you could call `get_args` and `get_query_args` methods
|
||||||
|
with the new values.
|
||||||
|
|
||||||
|
For the queryset `/?test1=value1&test2=&test3=value3`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
@app.route("/query_string")
|
||||||
|
def query_string(request):
|
||||||
|
args_with_blank_values = request.get_args(keep_blank_values=True)
|
||||||
|
return json({
|
||||||
|
"parsed": True,
|
||||||
|
"url": request.url,
|
||||||
|
"args_with_blank_values": args_with_blank_values,
|
||||||
|
"query_string": request.query_string
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The output will be:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"parsed": true,
|
||||||
|
"url": "http:\/\/0.0.0.0:8000\/query_string?test1=value1&test2=&test3=value3",
|
||||||
|
"args_with_blank_values": {"test1": ["value1""], "test2": "", "test3": ["value3"]},
|
||||||
|
"query_string": "test1=value1&test2=&test3=value3"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Accessing values using `get` and `getlist`
|
## Accessing values using `get` and `getlist`
|
||||||
|
|
||||||
The request properties which return a dictionary actually return a subclass of
|
The request properties which return a dictionary actually return a subclass of
|
||||||
|
|
123
sanic/request.py
123
sanic/request.py
|
@ -2,11 +2,12 @@ import asyncio
|
||||||
import email.utils
|
import email.utils
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
import warnings
|
||||||
|
|
||||||
from cgi import parse_header
|
from cgi import parse_header
|
||||||
from collections import namedtuple
|
from collections import defaultdict, namedtuple
|
||||||
from http.cookies import SimpleCookie
|
from http.cookies import SimpleCookie
|
||||||
from urllib.parse import parse_qs, unquote, urlunparse
|
from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse
|
||||||
|
|
||||||
from httptools import parse_url
|
from httptools import parse_url
|
||||||
|
|
||||||
|
@ -83,6 +84,7 @@ class Request(dict):
|
||||||
"headers",
|
"headers",
|
||||||
"method",
|
"method",
|
||||||
"parsed_args",
|
"parsed_args",
|
||||||
|
"parsed_not_grouped_args",
|
||||||
"parsed_files",
|
"parsed_files",
|
||||||
"parsed_form",
|
"parsed_form",
|
||||||
"parsed_json",
|
"parsed_json",
|
||||||
|
@ -109,7 +111,8 @@ class Request(dict):
|
||||||
self.parsed_json = None
|
self.parsed_json = None
|
||||||
self.parsed_form = None
|
self.parsed_form = None
|
||||||
self.parsed_files = None
|
self.parsed_files = None
|
||||||
self.parsed_args = None
|
self.parsed_args = defaultdict(RequestParameters)
|
||||||
|
self.parsed_not_grouped_args = defaultdict(list)
|
||||||
self.uri_template = None
|
self.uri_template = None
|
||||||
self._cookies = None
|
self._cookies = None
|
||||||
self.stream = None
|
self.stream = None
|
||||||
|
@ -199,21 +202,117 @@ class Request(dict):
|
||||||
|
|
||||||
return self.parsed_files
|
return self.parsed_files
|
||||||
|
|
||||||
@property
|
def get_args(
|
||||||
def args(self):
|
self,
|
||||||
if self.parsed_args is None:
|
keep_blank_values: bool = False,
|
||||||
|
strict_parsing: bool = False,
|
||||||
|
encoding: str = "utf-8",
|
||||||
|
errors: str = "replace",
|
||||||
|
) -> RequestParameters:
|
||||||
|
"""
|
||||||
|
Method to parse `query_string` using `urllib.parse.parse_qs`.
|
||||||
|
This methods is used by `args` property.
|
||||||
|
Can be used directly if you need to change default parameters.
|
||||||
|
:param keep_blank_values: flag indicating whether blank values in
|
||||||
|
percent-encoded queries should be treated as blank strings.
|
||||||
|
A true value indicates that blanks should be retained as blank
|
||||||
|
strings. The default false value indicates that blank values
|
||||||
|
are to be ignored and treated as if they were not included.
|
||||||
|
:type keep_blank_values: bool
|
||||||
|
:param strict_parsing: flag indicating what to do with parsing errors.
|
||||||
|
If false (the default), errors are silently ignored. If true,
|
||||||
|
errors raise a ValueError exception.
|
||||||
|
:type strict_parsing: bool
|
||||||
|
:param encoding: specify how to decode percent-encoded sequences
|
||||||
|
into Unicode characters, as accepted by the bytes.decode() method.
|
||||||
|
:type encoding: str
|
||||||
|
:param errors: specify how to decode percent-encoded sequences
|
||||||
|
into Unicode characters, as accepted by the bytes.decode() method.
|
||||||
|
:type errors: str
|
||||||
|
:return: RequestParameters
|
||||||
|
"""
|
||||||
|
if not self.parsed_args[
|
||||||
|
(keep_blank_values, strict_parsing, encoding, errors)
|
||||||
|
]:
|
||||||
if self.query_string:
|
if self.query_string:
|
||||||
self.parsed_args = RequestParameters(
|
self.parsed_args[
|
||||||
parse_qs(self.query_string)
|
(keep_blank_values, strict_parsing, encoding, errors)
|
||||||
|
] = RequestParameters(
|
||||||
|
parse_qs(
|
||||||
|
qs=self.query_string,
|
||||||
|
keep_blank_values=keep_blank_values,
|
||||||
|
strict_parsing=strict_parsing,
|
||||||
|
encoding=encoding,
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
self.parsed_args = RequestParameters()
|
return self.parsed_args[
|
||||||
return self.parsed_args
|
(keep_blank_values, strict_parsing, encoding, errors)
|
||||||
|
]
|
||||||
|
|
||||||
|
args = property(get_args)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def raw_args(self):
|
def raw_args(self) -> dict:
|
||||||
|
if self.app.debug: # pragma: no cover
|
||||||
|
warnings.simplefilter("default")
|
||||||
|
warnings.warn(
|
||||||
|
"Use of raw_args will be deprecated in "
|
||||||
|
"the future versions. Please use args or query_args "
|
||||||
|
"properties instead",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
return {k: v[0] for k, v in self.args.items()}
|
return {k: v[0] for k, v in self.args.items()}
|
||||||
|
|
||||||
|
def get_query_args(
|
||||||
|
self,
|
||||||
|
keep_blank_values: bool = False,
|
||||||
|
strict_parsing: bool = False,
|
||||||
|
encoding: str = "utf-8",
|
||||||
|
errors: str = "replace",
|
||||||
|
) -> list:
|
||||||
|
"""
|
||||||
|
Method to parse `query_string` using `urllib.parse.parse_qsl`.
|
||||||
|
This methods is used by `query_args` property.
|
||||||
|
Can be used directly if you need to change default parameters.
|
||||||
|
:param keep_blank_values: flag indicating whether blank values in
|
||||||
|
percent-encoded queries should be treated as blank strings.
|
||||||
|
A true value indicates that blanks should be retained as blank
|
||||||
|
strings. The default false value indicates that blank values
|
||||||
|
are to be ignored and treated as if they were not included.
|
||||||
|
:type keep_blank_values: bool
|
||||||
|
:param strict_parsing: flag indicating what to do with parsing errors.
|
||||||
|
If false (the default), errors are silently ignored. If true,
|
||||||
|
errors raise a ValueError exception.
|
||||||
|
:type strict_parsing: bool
|
||||||
|
:param encoding: specify how to decode percent-encoded sequences
|
||||||
|
into Unicode characters, as accepted by the bytes.decode() method.
|
||||||
|
:type encoding: str
|
||||||
|
:param errors: specify how to decode percent-encoded sequences
|
||||||
|
into Unicode characters, as accepted by the bytes.decode() method.
|
||||||
|
:type errors: str
|
||||||
|
:return: list
|
||||||
|
"""
|
||||||
|
if not self.parsed_not_grouped_args[
|
||||||
|
(keep_blank_values, strict_parsing, encoding, errors)
|
||||||
|
]:
|
||||||
|
if self.query_string:
|
||||||
|
self.parsed_not_grouped_args[
|
||||||
|
(keep_blank_values, strict_parsing, encoding, errors)
|
||||||
|
] = parse_qsl(
|
||||||
|
qs=self.query_string,
|
||||||
|
keep_blank_values=keep_blank_values,
|
||||||
|
strict_parsing=strict_parsing,
|
||||||
|
encoding=encoding,
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
return self.parsed_not_grouped_args[
|
||||||
|
(keep_blank_values, strict_parsing, encoding, errors)
|
||||||
|
]
|
||||||
|
|
||||||
|
query_args = property(get_query_args)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cookies(self):
|
def cookies(self):
|
||||||
if self._cookies is None:
|
if self._cookies is None:
|
||||||
|
|
|
@ -10,7 +10,7 @@ import pytest
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic import Blueprint
|
from sanic import Blueprint
|
||||||
from sanic.exceptions import ServerError
|
from sanic.exceptions import ServerError
|
||||||
from sanic.request import DEFAULT_HTTP_CONTENT_TYPE
|
from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, RequestParameters
|
||||||
from sanic.response import json, text
|
from sanic.response import json, text
|
||||||
from sanic.testing import HOST, PORT
|
from sanic.testing import HOST, PORT
|
||||||
|
|
||||||
|
@ -130,6 +130,9 @@ def test_query_string(app):
|
||||||
|
|
||||||
assert request.args.get("test1") == "1"
|
assert request.args.get("test1") == "1"
|
||||||
assert request.args.get("test2") == "false"
|
assert request.args.get("test2") == "false"
|
||||||
|
assert request.args.getlist("test2") == ["false", "true"]
|
||||||
|
assert request.args.getlist("test1") == ["1"]
|
||||||
|
assert request.args.get("test3", default="My value") == "My value"
|
||||||
|
|
||||||
|
|
||||||
def test_uri_template(app):
|
def test_uri_template(app):
|
||||||
|
@ -646,6 +649,77 @@ def test_request_raw_args(app):
|
||||||
assert request.raw_args == params
|
assert request.raw_args == params
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_query_args(app):
|
||||||
|
# test multiple params with the same key
|
||||||
|
params = [('test', 'value1'), ('test', 'value2')]
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def handler(request):
|
||||||
|
return text("pass")
|
||||||
|
|
||||||
|
request, response = app.test_client.get("/", params=params)
|
||||||
|
|
||||||
|
assert request.query_args == params
|
||||||
|
|
||||||
|
# test cached value
|
||||||
|
assert request.parsed_not_grouped_args[(False, False, "utf-8", "replace")] == request.query_args
|
||||||
|
|
||||||
|
# test params directly in the url
|
||||||
|
request, response = app.test_client.get("/?test=value1&test=value2")
|
||||||
|
|
||||||
|
assert request.query_args == params
|
||||||
|
|
||||||
|
# test unique params
|
||||||
|
params = [('test1', 'value1'), ('test2', 'value2')]
|
||||||
|
|
||||||
|
request, response = app.test_client.get("/", params=params)
|
||||||
|
|
||||||
|
assert request.query_args == params
|
||||||
|
|
||||||
|
# test no params
|
||||||
|
request, response = app.test_client.get("/")
|
||||||
|
|
||||||
|
assert not request.query_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_query_args_custom_parsing(app):
|
||||||
|
@app.get("/")
|
||||||
|
def handler(request):
|
||||||
|
return text("pass")
|
||||||
|
|
||||||
|
request, response = app.test_client.get("/?test1=value1&test2=&test3=value3")
|
||||||
|
|
||||||
|
assert request.get_query_args(
|
||||||
|
keep_blank_values=True
|
||||||
|
) == [
|
||||||
|
('test1', 'value1'), ('test2', ''), ('test3', 'value3')
|
||||||
|
]
|
||||||
|
assert request.query_args == [
|
||||||
|
('test1', 'value1'), ('test3', 'value3')
|
||||||
|
]
|
||||||
|
assert request.get_query_args(
|
||||||
|
keep_blank_values=False
|
||||||
|
) == [
|
||||||
|
('test1', 'value1'), ('test3', 'value3')
|
||||||
|
]
|
||||||
|
|
||||||
|
assert request.get_args(
|
||||||
|
keep_blank_values=True
|
||||||
|
) == RequestParameters(
|
||||||
|
{"test1": ["value1"], "test2": [""], "test3": ["value3"]}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert request.args == RequestParameters(
|
||||||
|
{"test1": ["value1"], "test3": ["value3"]}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert request.get_args(
|
||||||
|
keep_blank_values=False
|
||||||
|
) == RequestParameters(
|
||||||
|
{"test1": ["value1"], "test3": ["value3"]}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_request_cookies(app):
|
def test_request_cookies(app):
|
||||||
|
|
||||||
cookies = {"test": "OK"}
|
cookies = {"test": "OK"}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user