From: RafaelWO <38643099+RafaelWO@users.noreply.github.com> Date: Fri, 1 Nov 2024 12:54:13 +0000 (+0100) Subject: Move utility functions from _utils.py to _multipart.py (#3388) X-Git-Tag: 0.28.0~10 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6212e8fa3bf90154b891c7721f6d29737caac861;p=thirdparty%2Fhttpx.git Move utility functions from _utils.py to _multipart.py (#3388) --- diff --git a/httpx/_multipart.py b/httpx/_multipart.py index 8edb6227..b4761af9 100644 --- a/httpx/_multipart.py +++ b/httpx/_multipart.py @@ -1,7 +1,9 @@ from __future__ import annotations import io +import mimetypes import os +import re import typing from pathlib import Path @@ -14,13 +16,42 @@ from ._types import ( SyncByteStream, ) from ._utils import ( - format_form_param, - guess_content_type, peek_filelike_length, primitive_value_to_str, to_bytes, ) +_HTML5_FORM_ENCODING_REPLACEMENTS = {'"': "%22", "\\": "\\\\"} +_HTML5_FORM_ENCODING_REPLACEMENTS.update( + {chr(c): "%{:02X}".format(c) for c in range(0x1F + 1) if c != 0x1B} +) +_HTML5_FORM_ENCODING_RE = re.compile( + r"|".join([re.escape(c) for c in _HTML5_FORM_ENCODING_REPLACEMENTS.keys()]) +) + + +def _format_form_param(name: str, value: str) -> bytes: + """ + Encode a name/value pair within a multipart form. + """ + + def replacer(match: typing.Match[str]) -> str: + return _HTML5_FORM_ENCODING_REPLACEMENTS[match.group(0)] + + value = _HTML5_FORM_ENCODING_RE.sub(replacer, value) + return f'{name}="{value}"'.encode() + + +def _guess_content_type(filename: str | None) -> str | None: + """ + Guesses the mimetype based on a filename. Defaults to `application/octet-stream`. + + Returns `None` if `filename` is `None` or empty. + """ + if filename: + return mimetypes.guess_type(filename)[0] or "application/octet-stream" + return None + def get_multipart_boundary_from_content_type( content_type: bytes | None, @@ -58,7 +89,7 @@ class DataField: def render_headers(self) -> bytes: if not hasattr(self, "_headers"): - name = format_form_param("name", self.name) + name = _format_form_param("name", self.name) self._headers = b"".join( [b"Content-Disposition: form-data; ", name, b"\r\n\r\n"] ) @@ -115,7 +146,7 @@ class FileField: fileobj = value if content_type is None: - content_type = guess_content_type(filename) + content_type = _guess_content_type(filename) has_content_type_header = any("content-type" in key.lower() for key in headers) if content_type is not None and not has_content_type_header: @@ -156,10 +187,10 @@ class FileField: if not hasattr(self, "_headers"): parts = [ b"Content-Disposition: form-data; ", - format_form_param("name", self.name), + _format_form_param("name", self.name), ] if self.filename: - filename = format_form_param("filename", self.filename) + filename = _format_form_param("filename", self.filename) parts.extend([b"; ", filename]) for header_name, header_value in self.headers.items(): key, val = f"\r\n{header_name}: ".encode(), header_value.encode() diff --git a/httpx/_utils.py b/httpx/_utils.py index 1c959e65..c873bdb2 100644 --- a/httpx/_utils.py +++ b/httpx/_utils.py @@ -3,7 +3,6 @@ from __future__ import annotations import codecs import email.message import ipaddress -import mimetypes import os import re import typing @@ -15,15 +14,6 @@ if typing.TYPE_CHECKING: # pragma: no cover from ._urls import URL -_HTML5_FORM_ENCODING_REPLACEMENTS = {'"': "%22", "\\": "\\\\"} -_HTML5_FORM_ENCODING_REPLACEMENTS.update( - {chr(c): "%{:02X}".format(c) for c in range(0x1F + 1) if c != 0x1B} -) -_HTML5_FORM_ENCODING_RE = re.compile( - r"|".join([re.escape(c) for c in _HTML5_FORM_ENCODING_REPLACEMENTS.keys()]) -) - - def primitive_value_to_str(value: PrimitiveData) -> str: """ Coerce a primitive data type into a string value. @@ -50,18 +40,6 @@ def is_known_encoding(encoding: str) -> bool: return True -def format_form_param(name: str, value: str) -> bytes: - """ - Encode a name/value pair within a multipart form. - """ - - def replacer(match: typing.Match[str]) -> str: - return _HTML5_FORM_ENCODING_REPLACEMENTS[match.group(0)] - - value = _HTML5_FORM_ENCODING_RE.sub(replacer, value) - return f'{name}="{value}"'.encode() - - def parse_header_links(value: str) -> list[dict[str, str]]: """ Returns a list of parsed link headers, for more info see: @@ -216,12 +194,6 @@ def unquote(value: str) -> str: return value[1:-1] if value[0] == value[-1] == '"' else value -def guess_content_type(filename: str | None) -> str | None: - if filename: - return mimetypes.guess_type(filename)[0] or "application/octet-stream" - return None - - def peek_filelike_length(stream: typing.Any) -> int | None: """ Given a file-like stream object, return its length in number of bytes