Preserve blank form values for urlencoded forms (option) (#2439)

* task(request.form): Add tests for blank values

* fix(request): abstract form property to implement get_form(), allow for preserving of blanks

* fix(request): hinting for parsed_form

* fix(request): typing for parsed_files

* fix(request): ignore type assumption

* fix(request): mypy typechecking caused E501 when type set to ignore

* fix(request): mypy is too stupid to parse continuations

* fix(request): formatting

* fix(request): fix annotation and return for get_form()

* fix(request): linting, hinting
This commit is contained in:
Stephen Sadowski 2022-04-24 15:01:35 -05:00 committed by GitHub
parent 3a6cc7389c
commit 78b6723149
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 99 additions and 21 deletions

View File

@ -152,8 +152,8 @@ class Request:
self.parsed_accept: Optional[AcceptContainer] = None self.parsed_accept: Optional[AcceptContainer] = None
self.parsed_credentials: Optional[Credentials] = None self.parsed_credentials: Optional[Credentials] = None
self.parsed_json = None self.parsed_json = None
self.parsed_form = None self.parsed_form: Optional[RequestParameters] = None
self.parsed_files = None self.parsed_files: Optional[RequestParameters] = None
self.parsed_token: Optional[str] = None self.parsed_token: Optional[str] = None
self.parsed_args: DefaultDict[ self.parsed_args: DefaultDict[
Tuple[bool, bool, str, str], RequestParameters Tuple[bool, bool, str, str], RequestParameters
@ -426,9 +426,9 @@ class Request:
pass pass
return self.parsed_credentials return self.parsed_credentials
@property def get_form(
def form(self): self, keep_blank_values: bool = False
if self.parsed_form is None: ) -> Optional[RequestParameters]:
self.parsed_form = RequestParameters() self.parsed_form = RequestParameters()
self.parsed_files = RequestParameters() self.parsed_files = RequestParameters()
content_type = self.headers.getone( content_type = self.headers.getone(
@ -438,11 +438,16 @@ class Request:
try: try:
if content_type == "application/x-www-form-urlencoded": if content_type == "application/x-www-form-urlencoded":
self.parsed_form = RequestParameters( self.parsed_form = RequestParameters(
parse_qs(self.body.decode("utf-8")) parse_qs(
self.body.decode("utf-8"),
keep_blank_values=keep_blank_values,
)
) )
elif content_type == "multipart/form-data": elif content_type == "multipart/form-data":
# TODO: Stream this instead of reading to/from memory # TODO: Stream this instead of reading to/from memory
boundary = parameters["boundary"].encode("utf-8") boundary = parameters["boundary"].encode( # type: ignore
"utf-8"
) # type: ignore
self.parsed_form, self.parsed_files = parse_multipart_form( self.parsed_form, self.parsed_files = parse_multipart_form(
self.body, boundary self.body, boundary
) )
@ -451,6 +456,13 @@ class Request:
return self.parsed_form return self.parsed_form
@property
def form(self):
if self.parsed_form is None:
self.get_form()
return self.parsed_form
@property @property
def files(self): def files(self):
if self.parsed_files is None: if self.parsed_files is None:

View File

@ -1016,6 +1016,72 @@ async def test_post_form_urlencoded_asgi(app):
assert request.form.get("test") == "OK" # For request.parsed_form assert request.form.get("test") == "OK" # For request.parsed_form
def test_post_form_urlencoded_keep_blanks(app):
@app.route("/", methods=["POST"])
async def handler(request):
request.get_form(keep_blank_values=True)
return text("OK")
payload = "test="
headers = {"content-type": "application/x-www-form-urlencoded"}
request, response = app.test_client.post(
"/", data=payload, headers=headers
)
assert request.form.get("test") == ""
assert request.form.get("test") == "" # For request.parsed_form
@pytest.mark.asyncio
async def test_post_form_urlencoded_keep_blanks_asgi(app):
@app.route("/", methods=["POST"])
async def handler(request):
request.get_form(keep_blank_values=True)
return text("OK")
payload = "test="
headers = {"content-type": "application/x-www-form-urlencoded"}
request, response = await app.asgi_client.post(
"/", data=payload, headers=headers
)
assert request.form.get("test") == ""
assert request.form.get("test") == "" # For request.parsed_form
def test_post_form_urlencoded_drop_blanks(app):
@app.route("/", methods=["POST"])
async def handler(request):
return text("OK")
payload = "test="
headers = {"content-type": "application/x-www-form-urlencoded"}
request, response = app.test_client.post(
"/", data=payload, headers=headers
)
assert "test" not in request.form.keys()
@pytest.mark.asyncio
async def test_post_form_urlencoded_drop_blanks_asgi(app):
@app.route("/", methods=["POST"])
async def handler(request):
return text("OK")
payload = "test="
headers = {"content-type": "application/x-www-form-urlencoded"}
request, response = await app.asgi_client.post(
"/", data=payload, headers=headers
)
assert "test" not in request.form.keys()
@pytest.mark.parametrize( @pytest.mark.parametrize(
"payload", "payload",
[ [