Skip to content

Commit

Permalink
♻️ Move functions from _utils.py to _multipart.py
Browse files Browse the repository at this point in the history
The following functions are moved because they are only used in _multipart.py
* format_form_param
* guess_content_type
  • Loading branch information
RafaelWO committed Nov 1, 2024
1 parent 83a8518 commit d51b76a
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 34 deletions.
43 changes: 37 additions & 6 deletions httpx/_multipart.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from __future__ import annotations

import io
import mimetypes
import os
import re
import typing
from pathlib import Path

Expand All @@ -14,13 +16,42 @@
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,
Expand Down Expand Up @@ -58,7 +89,7 @@ def __init__(self, name: str, value: str | bytes | int | float | None) -> None:

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"]
)
Expand Down Expand Up @@ -115,7 +146,7 @@ def __init__(self, name: str, value: FileTypes) -> None:
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:
Expand Down Expand Up @@ -156,10 +187,10 @@ def render_headers(self) -> bytes:
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()
Expand Down
28 changes: 0 additions & 28 deletions httpx/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import codecs
import email.message
import ipaddress
import mimetypes
import os
import re
import typing
Expand All @@ -15,15 +14,6 @@
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.
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit d51b76a

Please sign in to comment.