Skip to content

Commit 12b3f26

Browse files
committed
Add content_type property to File
Makes all headers lower case, fixing case sensitivity issues. Exposes jheaders property in Files and Fields.
1 parent d46a9c6 commit 12b3f26

12 files changed

+151
-27
lines changed

python_multipart/multipart.py

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,14 @@ def finalize(self) -> None: ...
6868
def close(self) -> None: ...
6969

7070
class FieldProtocol(_FormProtocol, Protocol):
71-
def __init__(self, name: bytes, headers: dict[str,bytes]) -> None:
72-
...
71+
def __init__(self, name: bytes, headers: dict[str, bytes]) -> None: ...
7372

7473
def set_none(self) -> None: ...
7574

7675
class FileProtocol(_FormProtocol, Protocol):
77-
def __init__(self, file_name: bytes | None, field_name: bytes | None, headers: dict[str,bytes], config: FileConfig) -> None:
78-
...
76+
def __init__(
77+
self, file_name: bytes | None, field_name: bytes | None, config: FileConfig, headers: dict[str, bytes]
78+
) -> None: ...
7979

8080
OnFieldCallback = Callable[[FieldProtocol], None]
8181
OnFileCallback = Callable[[FileProtocol], None]
@@ -225,10 +225,10 @@ class Field:
225225
name: The name of the form field.
226226
"""
227227

228-
def __init__(self, name: bytes, headers: dict[str,bytes]={}) -> None:
228+
def __init__(self, name: bytes, headers: dict[str, bytes] = {}) -> None:
229229
self._name = name
230230
self._value: list[bytes] = []
231-
self._headers: dict[str,bytes] = headers
231+
self._headers: dict[str, bytes] = headers
232232

233233
# We cache the joined version of _value for speed.
234234
self._cache = _missing
@@ -321,7 +321,7 @@ def value(self) -> bytes | None:
321321
return self._cache
322322

323323
@property
324-
def headers(self) -> dict[str,bytes]:
324+
def headers(self) -> dict[str, bytes]:
325325
"""This property returns the headers of the field."""
326326
return self._headers
327327

@@ -365,7 +365,13 @@ class File:
365365
config: The configuration for this File. See above for valid configuration keys and their corresponding values.
366366
""" # noqa: E501
367367

368-
def __init__(self, file_name: bytes | None, field_name: bytes | None = None, headers: dict[str,bytes] = {}, config: FileConfig = {}) -> None:
368+
def __init__(
369+
self,
370+
file_name: bytes | None,
371+
field_name: bytes | None = None,
372+
headers: dict[str, bytes] = {},
373+
config: FileConfig = {},
374+
) -> None:
369375
# Save configuration, set other variables default.
370376
self.logger = logging.getLogger(__name__)
371377
self._config = config
@@ -430,11 +436,15 @@ def in_memory(self) -> bool:
430436
return self._in_memory
431437

432438
@property
433-
def headers(self) -> dict[str,bytes]:
434-
"""The headers for this part.
435-
"""
439+
def headers(self) -> dict[str, bytes]:
440+
"""The headers for this part."""
436441
return self._headers
437-
442+
443+
@property
444+
def content_type(self) -> bytes | None:
445+
"""The Content-Type value for this part."""
446+
return self._headers.get("content-type")
447+
438448
def flush_to_disk(self) -> None:
439449
"""If the file is already on-disk, do nothing. Otherwise, copy from
440450
the in-memory buffer to a disk file, and then reassign our internal
@@ -1555,7 +1565,7 @@ def __init__(
15551565

15561566
def on_start() -> None:
15571567
nonlocal file
1558-
file = FileClass(file_name, None, config=cast("FileConfig", self.config))
1568+
file = FileClass(file_name, None, headers={}, config=cast("FileConfig", self.config))
15591569

15601570
def on_data(data: bytes, start: int, end: int) -> None:
15611571
nonlocal file
@@ -1594,7 +1604,7 @@ def on_field_name(data: bytes, start: int, end: int) -> None:
15941604
def on_field_data(data: bytes, start: int, end: int) -> None:
15951605
nonlocal f
15961606
if f is None:
1597-
f = FieldClass(b"".join(name_buffer))
1607+
f = FieldClass(b"".join(name_buffer), headers={})
15981608
del name_buffer[:]
15991609
f.write(data[start:end])
16001610

@@ -1604,7 +1614,7 @@ def on_field_end() -> None:
16041614
if f is None:
16051615
# If we get here, it's because there was no field data.
16061616
# We create a field, set it to None, and then continue.
1607-
f = FieldClass(b"".join(name_buffer))
1617+
f = FieldClass(b"".join(name_buffer), headers={})
16081618
del name_buffer[:]
16091619
f.set_none()
16101620

@@ -1636,7 +1646,7 @@ def _on_end() -> None:
16361646

16371647
header_name: list[bytes] = []
16381648
header_value: list[bytes] = []
1639-
headers: dict[bytes, bytes] = {}
1649+
headers: dict[str, bytes] = {}
16401650

16411651
f_multi: FileProtocol | FieldProtocol | None = None
16421652
writer = None
@@ -1671,7 +1681,7 @@ def on_header_value(data: bytes, start: int, end: int) -> None:
16711681
header_value.append(data[start:end])
16721682

16731683
def on_header_end() -> None:
1674-
headers[b"".join(header_name)] = b"".join(header_value)
1684+
headers[b"".join(header_name).decode().lower()] = b"".join(header_value)
16751685
del header_name[:]
16761686
del header_value[:]
16771687

@@ -1681,26 +1691,25 @@ def on_headers_finished() -> None:
16811691
is_file = False
16821692

16831693
# Parse the content-disposition header.
1684-
# TODO: handle mixed case
1685-
content_disp = headers.get(b"Content-Disposition")
1694+
content_disp = headers.get("content-disposition")
16861695
disp, options = parse_options_header(content_disp)
16871696

16881697
# Get the field and filename.
1689-
field_name = options.get(b"name")
1698+
field_name = options.get(b"name", b"")
16901699
file_name = options.get(b"filename")
16911700
# TODO: check for errors
16921701

16931702
# Create the proper class.
16941703
if file_name is None:
1695-
f_multi = FieldClass(field_name)
1704+
f_multi = FieldClass(field_name, headers=headers)
16961705
else:
1697-
f_multi = FileClass(file_name, field_name, config=cast("FileConfig", self.config))
1706+
f_multi = FileClass(file_name, field_name, config=cast("FileConfig", self.config), headers=headers)
16981707
is_file = True
16991708

17001709
# Parse the given Content-Transfer-Encoding to determine what
17011710
# we need to do with the incoming data.
17021711
# TODO: check that we properly handle 8bit / 7bit encoding.
1703-
transfer_encoding = headers.get(b"Content-Transfer-Encoding", b"7bit")
1712+
transfer_encoding = headers.get("content-transfer-encoding", b"7bit")
17041713

17051714
if transfer_encoding in (b"binary", b"8bit", b"7bit"):
17061715
writer = f_multi

tests/test_data/http/almost_match_boundary.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ expected:
33
- name: file
44
type: file
55
file_name: test.txt
6+
content_type: text/plain
67
data: !!binary |
78
LS1ib3VuZGFyaQ0KLS1ib3VuZGFyeXEtLWJvdW5kYXJ5DXEtLWJvdW5kYXJxDQotLWJvdW5hcnlkLS0NCi0tbm90Ym91bmQtLQ0KLS1taXNtYXRjaA0KLS1taXNtYXRjaC0tDQotLWJvdW5kYXJ5LVENCi0tYm91bmRhcnkNUS0tYm91bmRhcnlR
89

tests/test_data/http/base64_encoding.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ expected:
33
- name: file
44
type: file
55
file_name: test.txt
6+
content_type: text/plain
67
data: !!binary |
78
VGVzdCAxMjM=
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
------WebKitFormBoundarygbACTUR58IyeurVf
2+
Content-Disposition: form-data; name="file1"; filename="test1.txt"
3+
Content-Type: text/plain
4+
5+
Test file #1
6+
------WebKitFormBoundarygbACTUR58IyeurVf
7+
CONTENT-DISPOSITION: form-data; name="file2"; filename="test2.txt"
8+
CONTENT-Type: text/plain
9+
10+
Test file #2
11+
------WebKitFormBoundarygbACTUR58IyeurVf
12+
content-disposition: form-data; name="file3"; filename="test3.txt"
13+
content-type: text/plain
14+
15+
Test file #3
16+
------WebKitFormBoundarygbACTUR58IyeurVf
17+
cOnTenT-DiSpOsItiOn: form-data; name="file4"; filename="test4.txt"
18+
Content-Type: text/plain
19+
20+
Test file #4
21+
------WebKitFormBoundarygbACTUR58IyeurVf--
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
boundary: ----WebKitFormBoundarygbACTUR58IyeurVf
2+
expected:
3+
- name: file1
4+
type: file
5+
file_name: test1.txt
6+
content_type: text/plain
7+
data: !!binary |
8+
VGVzdCBmaWxlICMx
9+
- name: file2
10+
type: file
11+
file_name: test2.txt
12+
content_type: text/plain
13+
data: !!binary |
14+
VGVzdCBmaWxlICMy
15+
- name: file3
16+
type: file
17+
file_name: test3.txt
18+
content_type: text/plain
19+
data: !!binary |
20+
VGVzdCBmaWxlICMz
21+
- name: file4
22+
type: file
23+
file_name: test4.txt
24+
content_type: text/plain
25+
data: !!binary |
26+
VGVzdCBmaWxlICM0

tests/test_data/http/header_with_number.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ expected:
33
- name: files
44
type: file
55
file_name: secret.txt
6+
content_type: "text/plain; charset=utf-8"
67
data: !!binary |
78
YWFhYWFh

tests/test_data/http/multiple_files.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ expected:
33
- name: file1
44
type: file
55
file_name: test1.txt
6+
content_type: 'text/plain'
67
data: !!binary |
78
VGVzdCBmaWxlICMx
89
- name: file2
910
type: file
1011
file_name: test2.txt
12+
content_type: 'text/plain'
1113
data: !!binary |
1214
VGVzdCBmaWxlICMy
1315

tests/test_data/http/quoted_printable_encoding.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ expected:
33
- name: file
44
type: file
55
file_name: test.txt
6+
content_type: 'text/plain'
67
data: !!binary |
78
Zm9vPWJhcg==

tests/test_data/http/single_field_single_file.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ boundary: boundary
22
expected:
33
- name: field
44
type: field
5+
content_type: 'text/plain'
56
data: !!binary |
67
dGVzdDE=
78
- name: file
89
type: file
910
file_name: file.txt
11+
content_type: 'text/plain'
1012
data: !!binary |
1113
dGVzdDI=
1214

tests/test_data/http/single_file.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ expected:
33
- name: file
44
type: file
55
file_name: test.txt
6+
content_type: 'text/plain'
67
data: !!binary |
78
VGhpcyBpcyBhIHRlc3QgZmlsZS4=
89

0 commit comments

Comments
 (0)