Remove deprecated iter_fields(), add type hints to 'fields' and 'filepost' modules
MODIFY
src/urllib3/fields.py
src/urllib3/fields.py
import email.utils
import mimetypes
import re
+from typing import (
+ Callable,
+ Dict,
+ Iterable,
+ Mapping,
+ Match,
+ Optional,
+ Sequence,
+ Tuple,
+ Union,
+ cast,
+)
+
+_TYPE_FIELD_VALUE = Union[str, bytes]
+_TYPE_FIELD_VALUE_TUPLE = Union[
+ _TYPE_FIELD_VALUE, Tuple[str, _TYPE_FIELD_VALUE], Tuple[str, _TYPE_FIELD_VALUE, str]
+]
-def guess_content_type(filename, default="application/octet-stream"):
+def guess_content_type(filename: str, default: str = "application/octet-stream") -> str:
"""
Guess the "Content-Type" of a file.
@@ -17,7 +34,7 @@ def guess_content_type(filename, default="application/octet-stream"):
return default
-def format_header_param_rfc2231(name, value):
+def format_header_param_rfc2231(name: str, value: Union[str, bytes]) -> str:
"""
Helper function to format and quote a single header parameter using the
strategy defined in RFC 2231.
@@ -63,8 +80,8 @@ _HTML5_REPLACEMENTS.update(
)
-def _replace_multiple(value, needles_and_replacements):
- def replacer(match):
+def _replace_multiple(value: str, needles_and_replacements: Mapping[str, str]) -> str:
+ def replacer(match: Match[str]) -> str:
return needles_and_replacements[match.group(0)]
pattern = re.compile(
@@ -76,7 +93,7 @@ def _replace_multiple(value, needles_and_replacements):
return result
-def format_header_param_html5(name, value):
+def format_header_param_html5(name: str, value: _TYPE_FIELD_VALUE) -> str:
"""
Helper function to format and quote a single header parameter using the
HTML5 strategy.
@@ -126,22 +143,31 @@ class RequestField:
def __init__(
self,
- name,
- data,
- filename=None,
- headers=None,
- header_formatter=format_header_param_html5,
+ name: str,
+ data: _TYPE_FIELD_VALUE,
+ filename: Optional[str] = None,
+ headers: Optional[Mapping[str, str]] = None,
+ header_formatter: Callable[
+ [str, _TYPE_FIELD_VALUE], str
+ ] = format_header_param_html5,
):
self._name = name
self._filename = filename
self.data = data
- self.headers = {}
+ self.headers: Dict[str, Optional[str]] = {}
if headers:
self.headers = dict(headers)
self.header_formatter = header_formatter
@classmethod
- def from_tuples(cls, fieldname, value, header_formatter=format_header_param_html5):
+ def from_tuples(
+ cls,
+ fieldname: str,
+ value: _TYPE_FIELD_VALUE_TUPLE,
+ header_formatter: Callable[
+ [str, _TYPE_FIELD_VALUE], str
+ ] = format_header_param_html5,
+ ) -> "RequestField":
"""
A :class:`~urllib3.fields.RequestField` factory from old-style tuple parameters.
@@ -158,11 +184,17 @@ class RequestField:
Field names and filenames must be unicode.
"""
+ filename: Optional[str]
+ content_type: Optional[str]
+ data: _TYPE_FIELD_VALUE
+
if isinstance(value, tuple):
if len(value) == 3:
- filename, data, content_type = value
+ filename, data, content_type = cast(
+ Tuple[str, _TYPE_FIELD_VALUE, str], value
+ )
else:
- filename, data = value
+ filename, data = cast(Tuple[str, _TYPE_FIELD_VALUE], value)
content_type = guess_content_type(filename)
else:
filename = None
@@ -176,7 +208,7 @@ class RequestField:
return request_param
- def _render_part(self, name, value):
+ def _render_part(self, name: str, value: _TYPE_FIELD_VALUE) -> str:
"""
Overridable helper function to format a single header parameter. By
default, this calls ``self.header_formatter``.
@@ -189,7 +221,13 @@ class RequestField:
return self.header_formatter(name, value)
- def _render_parts(self, header_parts):
+ def _render_parts(
+ self,
+ header_parts: Union[
+ Dict[str, Optional[_TYPE_FIELD_VALUE]],
+ Sequence[Tuple[str, Optional[_TYPE_FIELD_VALUE]]],
+ ],
+ ) -> str:
"""
Helper function to format and quote a single header.
@@ -200,10 +238,13 @@ class RequestField:
A sequence of (k, v) tuples or a :class:`dict` of (k, v) to format
as `k1="v1"; k2="v2"; ...`.
"""
+ iterable: Iterable[Tuple[str, Optional[_TYPE_FIELD_VALUE]]]
+
parts = []
- iterable = header_parts
if isinstance(header_parts, dict):
iterable = header_parts.items()
+ else:
+ iterable = header_parts
for name, value in iterable:
if value is not None:
@@ -211,7 +252,7 @@ class RequestField:
return "; ".join(parts)
- def render_headers(self):
+ def render_headers(self) -> str:
"""
Renders the headers for this request field.
"""
@@ -231,22 +272,26 @@ class RequestField:
return "\r\n".join(lines)
def make_multipart(
- self, content_disposition=None, content_type=None, content_location=None
- ):
+ self,
+ content_disposition: Optional[str] = None,
+ content_type: Optional[str] = None,
+ content_location: Optional[str] = None,
+ ) -> None:
"""
Makes this request field into a multipart request field.
This method overrides "Content-Disposition", "Content-Type" and
"Content-Location" headers to the request parameter.
+ :param content_disposition:
+ The 'Content-Disposition' of the request body. Defaults to 'form-data'
:param content_type:
The 'Content-Type' of the request body.
:param content_location:
The 'Content-Location' of the request body.
"""
- self.headers["Content-Disposition"] = content_disposition or "form-data"
- self.headers["Content-Disposition"] += "; ".join(
+ content_disposition = (content_disposition or "form-data") + "; ".join(
[
"",
self._render_parts(
@@ -254,5 +299,7 @@ class RequestField:
),
]
)
+
+ self.headers["Content-Disposition"] = content_disposition
self.headers["Content-Type"] = content_type
self.headers["Content-Location"] = content_location

DELETE
src/urllib3/fields.pyi
-# Stubs for requests.packages.urllib3.fields (Python 3.4)
-
-from typing import Any, Callable, Mapping, Optional
-
-def guess_content_type(filename: str, default: str) -> str: ...
-def format_header_param_rfc2231(name: str, value: str) -> str: ...
-def format_header_param_html5(name: str, value: str) -> str: ...
-def format_header_param(name: str, value: str) -> str: ...
-
-class RequestField:
- data: Any
- headers: Optional[Mapping[str, str]]
- def __init__(
- self,
- name: str,
- data: Any,
- filename: Optional[str],
- headers: Optional[Mapping[str, str]],
- header_formatter: Callable[[str, str], str],
- ) -> None: ...
- @classmethod
- def from_tuples(
- cls, fieldname: str, value: str, header_formatter: Callable[[str, str], str]
- ) -> RequestField: ...
- def render_headers(self) -> str: ...
- def make_multipart(
- self, content_disposition: str, content_type: str, content_location: str
- ) -> None: ...

MODIFY
src/urllib3/filepost.py
src/urllib3/filepost.py
@@ -2,20 +2,29 @@ import binascii
import codecs
import os
from io import BytesIO
+from typing import Dict, Iterable, Optional, Sequence, Tuple, Union
-from .fields import RequestField
+from .fields import _TYPE_FIELD_VALUE_TUPLE, RequestField
writer = codecs.lookup("utf-8")[3]
+_TYPE_FIELDS_SEQUENCE = Sequence[
+ Union[Tuple[str, _TYPE_FIELD_VALUE_TUPLE], RequestField]
+]
+_TYPE_FIELDS = Union[
+ _TYPE_FIELDS_SEQUENCE,
+ Dict[str, _TYPE_FIELD_VALUE_TUPLE],
+]
-def choose_boundary():
+
+def choose_boundary() -> str:
"""
Our embarrassingly-simple replacement for mimetools.choose_boundary.
"""
return binascii.hexlify(os.urandom(16)).decode()
-def iter_field_objects(fields):
+def iter_field_objects(fields: _TYPE_FIELDS) -> Iterable[RequestField]:
"""
Iterate over fields.
@@ -23,37 +32,23 @@ def iter_field_objects(fields):
:class:`~urllib3.fields.RequestField`.
"""
+ iterable: Iterable[Union[RequestField, Tuple[str, _TYPE_FIELD_VALUE_TUPLE]]]
+
if isinstance(fields, dict):
- i = fields.items()
+ iterable = fields.items()
else:
- i = iter(fields)
+ iterable = fields
- for field in i:
+ for field in iterable:
if isinstance(field, RequestField):
yield field
else:
yield RequestField.from_tuples(*field)
-def iter_fields(fields):
- """
- .. deprecated:: 1.6
-
- Iterate over fields.
-
- The addition of :class:`~urllib3.fields.RequestField` makes this function
- obsolete. Instead, use :func:`iter_field_objects`, which returns
- :class:`~urllib3.fields.RequestField` objects.
-
- Supports list of (k, v) tuples and dicts.
- """
- if isinstance(fields, dict):
- return ((k, v) for k, v in fields.items())
-
- return ((k, v) for k, v in fields)
-
-
-def encode_multipart_formdata(fields, boundary=None):
+def encode_multipart_formdata(
+ fields: _TYPE_FIELDS, boundary: Optional[str] = None
+) -> Tuple[bytes, str]:
"""
Encode a dictionary of ``fields`` using the multipart/form-data MIME format.
@@ -86,6 +81,6 @@ def encode_multipart_formdata(fields, boundary=None):
body.write(f"--{boundary}--\r\n".encode("latin-1"))
- content_type = str(f"multipart/form-data; boundary={boundary}")
+ content_type = f"multipart/form-data; boundary={boundary}"
return body.getvalue(), content_type

DELETE
src/urllib3/filepost.pyi
-from typing import Any, Generator, List, Mapping, Optional, Tuple, Union
-
-from . import fields
-
-RequestField = fields.RequestField
-Fields = Union[Mapping[str, str], List[Tuple[str]], List[RequestField]]
-Iterator = Generator[Tuple[str], None, None]
-
-writer: Any
-
-def choose_boundary() -> str: ...
-def iter_field_objects(fields: Fields) -> Iterator: ...
-def iter_fields(fields: Fields) -> Iterator: ...
-def encode_multipart_formdata(
- fields: Fields, boundary: Optional[str]
-) -> Tuple[str]: ...

MODIFY
test/test_filepost.py
test/test_filepost.py
import pytest
from urllib3.fields import RequestField
-from urllib3.filepost import encode_multipart_formdata, iter_fields
+from urllib3.filepost import encode_multipart_formdata
BOUNDARY = "!! test boundary !!"
BOUNDARY_BYTES = BOUNDARY.encode()
-class TestIterfields:
- def test_dict(self):
- for fieldname, value in iter_fields(dict(a="b")):
- assert (fieldname, value) == ("a", "b")
-
- assert list(sorted(iter_fields(dict(a="b", c="d")))) == [("a", "b"), ("c", "d")]
-
- def test_tuple_list(self):
- for fieldname, value in iter_fields([("a", "b")]):
- assert (fieldname, value) == ("a", "b")
-
- assert list(iter_fields([("a", "b"), ("c", "d")])) == [("a", "b"), ("c", "d")]
-
-
class TestMultipartEncoding:
@pytest.mark.parametrize(
"fields", [dict(k="v", k2="v2"), [("k", "v"), ("k2", "v2")]]

Remove deprecated iter_fields(), add type hints to 'fields' and 'filepost' modules (#2148)

Co-authored-by: Quentin Pradet <quentin.pradet@gmail.com>