Skip to content

Commit 6212e8f

Browse files
authored
Move utility functions from _utils.py to _multipart.py (#3388)
1 parent 83a8518 commit 6212e8f

2 files changed

Lines changed: 37 additions & 34 deletions

File tree

httpx/_multipart.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from __future__ import annotations
22

33
import io
4+
import mimetypes
45
import os
6+
import re
57
import typing
68
from pathlib import Path
79

@@ -14,13 +16,42 @@
1416
SyncByteStream,
1517
)
1618
from ._utils import (
17-
format_form_param,
18-
guess_content_type,
1919
peek_filelike_length,
2020
primitive_value_to_str,
2121
to_bytes,
2222
)
2323

24+
_HTML5_FORM_ENCODING_REPLACEMENTS = {'"': "%22", "\\": "\\\\"}
25+
_HTML5_FORM_ENCODING_REPLACEMENTS.update(
26+
{chr(c): "%{:02X}".format(c) for c in range(0x1F + 1) if c != 0x1B}
27+
)
28+
_HTML5_FORM_ENCODING_RE = re.compile(
29+
r"|".join([re.escape(c) for c in _HTML5_FORM_ENCODING_REPLACEMENTS.keys()])
30+
)
31+
32+
33+
def _format_form_param(name: str, value: str) -> bytes:
34+
"""
35+
Encode a name/value pair within a multipart form.
36+
"""
37+
38+
def replacer(match: typing.Match[str]) -> str:
39+
return _HTML5_FORM_ENCODING_REPLACEMENTS[match.group(0)]
40+
41+
value = _HTML5_FORM_ENCODING_RE.sub(replacer, value)
42+
return f'{name}="{value}"'.encode()
43+
44+
45+
def _guess_content_type(filename: str | None) -> str | None:
46+
"""
47+
Guesses the mimetype based on a filename. Defaults to `application/octet-stream`.
48+
49+
Returns `None` if `filename` is `None` or empty.
50+
"""
51+
if filename:
52+
return mimetypes.guess_type(filename)[0] or "application/octet-stream"
53+
return None
54+
2455

2556
def get_multipart_boundary_from_content_type(
2657
content_type: bytes | None,
@@ -58,7 +89,7 @@ def __init__(self, name: str, value: str | bytes | int | float | None) -> None:
5889

5990
def render_headers(self) -> bytes:
6091
if not hasattr(self, "_headers"):
61-
name = format_form_param("name", self.name)
92+
name = _format_form_param("name", self.name)
6293
self._headers = b"".join(
6394
[b"Content-Disposition: form-data; ", name, b"\r\n\r\n"]
6495
)
@@ -115,7 +146,7 @@ def __init__(self, name: str, value: FileTypes) -> None:
115146
fileobj = value
116147

117148
if content_type is None:
118-
content_type = guess_content_type(filename)
149+
content_type = _guess_content_type(filename)
119150

120151
has_content_type_header = any("content-type" in key.lower() for key in headers)
121152
if content_type is not None and not has_content_type_header:
@@ -156,10 +187,10 @@ def render_headers(self) -> bytes:
156187
if not hasattr(self, "_headers"):
157188
parts = [
158189
b"Content-Disposition: form-data; ",
159-
format_form_param("name", self.name),
190+
_format_form_param("name", self.name),
160191
]
161192
if self.filename:
162-
filename = format_form_param("filename", self.filename)
193+
filename = _format_form_param("filename", self.filename)
163194
parts.extend([b"; ", filename])
164195
for header_name, header_value in self.headers.items():
165196
key, val = f"\r\n{header_name}: ".encode(), header_value.encode()

httpx/_utils.py

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import codecs
44
import email.message
55
import ipaddress
6-
import mimetypes
76
import os
87
import re
98
import typing
@@ -15,15 +14,6 @@
1514
from ._urls import URL
1615

1716

18-
_HTML5_FORM_ENCODING_REPLACEMENTS = {'"': "%22", "\\": "\\\\"}
19-
_HTML5_FORM_ENCODING_REPLACEMENTS.update(
20-
{chr(c): "%{:02X}".format(c) for c in range(0x1F + 1) if c != 0x1B}
21-
)
22-
_HTML5_FORM_ENCODING_RE = re.compile(
23-
r"|".join([re.escape(c) for c in _HTML5_FORM_ENCODING_REPLACEMENTS.keys()])
24-
)
25-
26-
2717
def primitive_value_to_str(value: PrimitiveData) -> str:
2818
"""
2919
Coerce a primitive data type into a string value.
@@ -50,18 +40,6 @@ def is_known_encoding(encoding: str) -> bool:
5040
return True
5141

5242

53-
def format_form_param(name: str, value: str) -> bytes:
54-
"""
55-
Encode a name/value pair within a multipart form.
56-
"""
57-
58-
def replacer(match: typing.Match[str]) -> str:
59-
return _HTML5_FORM_ENCODING_REPLACEMENTS[match.group(0)]
60-
61-
value = _HTML5_FORM_ENCODING_RE.sub(replacer, value)
62-
return f'{name}="{value}"'.encode()
63-
64-
6543
def parse_header_links(value: str) -> list[dict[str, str]]:
6644
"""
6745
Returns a list of parsed link headers, for more info see:
@@ -216,12 +194,6 @@ def unquote(value: str) -> str:
216194
return value[1:-1] if value[0] == value[-1] == '"' else value
217195

218196

219-
def guess_content_type(filename: str | None) -> str | None:
220-
if filename:
221-
return mimetypes.guess_type(filename)[0] or "application/octet-stream"
222-
return None
223-
224-
225197
def peek_filelike_length(stream: typing.Any) -> int | None:
226198
"""
227199
Given a file-like stream object, return its length in number of bytes

0 commit comments

Comments
 (0)