Skip to content

Commit daf6a18

Browse files
committed
feat: add FormData class for form and multipart requests
Adds the FormData class (already in java/csharp) to support repeated keys in `form` and multiple files per field in `multipart` for APIRequestContext. Fixes: #2834 Fixes: #2784
1 parent 1f847dd commit daf6a18

10 files changed

Lines changed: 443 additions & 92 deletions

File tree

playwright/_impl/_fetch.py

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import base64
1616
import json
17+
import mimetypes
1718
import pathlib
1819
import typing
1920
from pathlib import Path
@@ -32,6 +33,7 @@
3233
)
3334
from playwright._impl._connection import ChannelOwner, from_channel
3435
from playwright._impl._errors import is_target_closed_error
36+
from playwright._impl._form_data import FormData
3537
from playwright._impl._helper import (
3638
Error,
3739
NameValue,
@@ -51,9 +53,9 @@
5153
from playwright._impl._playwright import Playwright
5254

5355

54-
FormType = Dict[str, Union[bool, float, str]]
56+
FormType = Union[Dict[str, Union[bool, float, str]], FormData]
5557
DataType = Union[Any, bytes, str]
56-
MultipartType = Dict[str, Union[bytes, bool, float, str, FilePayload]]
58+
MultipartType = Union[Dict[str, Union[bytes, bool, float, str, FilePayload]], FormData]
5759
ParamsType = Union[Dict[str, Union[bool, float, str]], str]
5860

5961

@@ -212,7 +214,7 @@ async def patch(
212214
headers: Headers = None,
213215
data: DataType = None,
214216
form: FormType = None,
215-
multipart: Dict[str, Union[bytes, bool, float, str, FilePayload]] = None,
217+
multipart: MultipartType = None,
216218
timeout: float = None,
217219
failOnStatusCode: bool = None,
218220
ignoreHTTPSErrors: bool = None,
@@ -241,7 +243,7 @@ async def put(
241243
headers: Headers = None,
242244
data: DataType = None,
243245
form: FormType = None,
244-
multipart: Dict[str, Union[bytes, bool, float, str, FilePayload]] = None,
246+
multipart: MultipartType = None,
245247
timeout: float = None,
246248
failOnStatusCode: bool = None,
247249
ignoreHTTPSErrors: bool = None,
@@ -270,7 +272,7 @@ async def post(
270272
headers: Headers = None,
271273
data: DataType = None,
272274
form: FormType = None,
273-
multipart: Dict[str, Union[bytes, bool, float, str, FilePayload]] = None,
275+
multipart: MultipartType = None,
274276
timeout: float = None,
275277
failOnStatusCode: bool = None,
276278
ignoreHTTPSErrors: bool = None,
@@ -300,7 +302,7 @@ async def fetch(
300302
headers: Headers = None,
301303
data: DataType = None,
302304
form: FormType = None,
303-
multipart: Dict[str, Union[bytes, bool, float, str, FilePayload]] = None,
305+
multipart: MultipartType = None,
304306
timeout: float = None,
305307
failOnStatusCode: bool = None,
306308
ignoreHTTPSErrors: bool = None,
@@ -341,7 +343,7 @@ async def _inner_fetch(
341343
data: DataType = None,
342344
params: ParamsType = None,
343345
form: FormType = None,
344-
multipart: Dict[str, Union[bytes, bool, float, str, FilePayload]] = None,
346+
multipart: MultipartType = None,
345347
timeout: float = None,
346348
failOnStatusCode: bool = None,
347349
ignoreHTTPSErrors: bool = None,
@@ -381,21 +383,36 @@ async def _inner_fetch(
381383
else:
382384
raise Error(f"Unsupported 'data' type: {type(data)}")
383385
elif form:
384-
form_data = object_to_array(form)
386+
if isinstance(form, FormData):
387+
form_data = []
388+
for fd_name, fd_value in form._fields:
389+
if isinstance(fd_value, (pathlib.Path, dict)):
390+
raise Error(
391+
f"Form field {fd_name!r} must be a string, number or boolean. Use 'multipart' for file uploads."
392+
)
393+
form_data.append(NameValue(name=fd_name, value=str(fd_value)))
394+
else:
395+
form_data = object_to_array(form)
385396
elif multipart:
386397
multipart_data = []
387-
# Convert file-like values to ServerFilePayload structs.
388-
for name, value in multipart.items():
389-
if is_file_payload(value):
390-
payload = cast(FilePayload, value)
391-
assert isinstance(
392-
payload["buffer"], bytes
393-
), f"Unexpected buffer type of 'data.{name}'"
398+
if isinstance(multipart, FormData):
399+
for fd_name, fd_value in multipart._fields:
394400
multipart_data.append(
395-
FormField(name=name, file=file_payload_to_json(payload))
401+
await _form_data_field_to_form_field(fd_name, fd_value)
396402
)
397-
elif isinstance(value, str):
398-
multipart_data.append(FormField(name=name, value=value))
403+
else:
404+
# Convert file-like values to ServerFilePayload structs.
405+
for name, value in multipart.items():
406+
if is_file_payload(value):
407+
payload = cast(FilePayload, value)
408+
assert isinstance(
409+
payload["buffer"], bytes
410+
), f"Unexpected buffer type of 'data.{name}'"
411+
multipart_data.append(
412+
FormField(name=name, file=file_payload_to_json(payload))
413+
)
414+
elif isinstance(value, str):
415+
multipart_data.append(FormField(name=name, value=value))
399416
if (
400417
post_data_buffer is None
401418
and json_data is None
@@ -450,6 +467,28 @@ def file_payload_to_json(payload: FilePayload) -> ServerFilePayload:
450467
)
451468

452469

470+
async def _form_data_field_to_form_field(name: str, value: Any) -> FormField:
471+
if isinstance(value, pathlib.Path):
472+
mime_type, _ = mimetypes.guess_type(str(value))
473+
return FormField(
474+
name=name,
475+
file=ServerFilePayload(
476+
name=value.name,
477+
mimeType=mime_type or "application/octet-stream",
478+
buffer=base64.b64encode(await async_readfile(str(value))).decode(),
479+
),
480+
)
481+
if is_file_payload(value):
482+
payload = cast(FilePayload, value)
483+
assert isinstance(
484+
payload["buffer"], bytes
485+
), f"Unexpected buffer type of form field {name!r}"
486+
return FormField(name=name, file=file_payload_to_json(payload))
487+
if isinstance(value, (str, int, float, bool)):
488+
return FormField(name=name, value=str(value))
489+
raise Error(f"Unsupported form field {name!r} value type: {type(value).__name__}")
490+
491+
453492
class APIResponse:
454493
def __init__(self, context: APIRequestContext, initializer: Dict) -> None:
455494
self._loop = context._loop

playwright/_impl/_form_data.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Copyright (c) Microsoft Corporation.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import pathlib
16+
from typing import List, Tuple, Union
17+
18+
from playwright._impl._api_structures import FilePayload
19+
20+
FormDataValue = Union[bool, float, str, pathlib.Path, FilePayload]
21+
22+
23+
class FormData:
24+
def __init__(self) -> None:
25+
self._fields: List[Tuple[str, FormDataValue]] = []
26+
27+
def set(self, name: str, value: FormDataValue) -> "FormData":
28+
self._fields = [(n, v) for (n, v) in self._fields if n != name]
29+
self._fields.append((name, value))
30+
return self
31+
32+
def append(self, name: str, value: FormDataValue) -> "FormData":
33+
self._fields.append((name, value))
34+
return self

playwright/async_api/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import playwright._impl._api_structures
2424
import playwright._impl._errors
25+
import playwright._impl._form_data
2526
import playwright.async_api._generated
2627
from playwright._impl._assertions import (
2728
APIResponseAssertions as APIResponseAssertionsImpl,
@@ -69,6 +70,7 @@
6970

7071
Cookie = playwright._impl._api_structures.Cookie
7172
FilePayload = playwright._impl._api_structures.FilePayload
73+
FormData = playwright._impl._form_data.FormData
7274
FloatRect = playwright._impl._api_structures.FloatRect
7375
Geolocation = playwright._impl._api_structures.Geolocation
7476
HttpCredentials = playwright._impl._api_structures.HttpCredentials
@@ -171,6 +173,7 @@ def __call__(
171173
"FileChooser",
172174
"FilePayload",
173175
"FloatRect",
176+
"FormData",
174177
"Frame",
175178
"FrameLocator",
176179
"Geolocation",

0 commit comments

Comments
 (0)