Add hasattr checks for remaining protocol isinstance checks (#7505)

This commit is contained in:
Nate Prewitt
2026-06-09 08:45:59 -07:00
committed by GitHub
parent 6f205ff422
commit 6f66281a1d
3 changed files with 24 additions and 7 deletions

View File

@@ -29,6 +29,11 @@ class SupportsRead(Protocol[_T_co]):
def read(self, length: int = ..., /) -> _T_co: ...
def has_read(obj: Any) -> TypeIs[SupportsRead[str | bytes]]:
"""Check if obj supports read, including __getattr__ based proxies."""
return isinstance(obj, SupportsRead) or hasattr(obj, "read")
@runtime_checkable
class SupportsItems(Protocol[_KT_co, _VT_co]):
def items(self) -> Iterable[tuple[_KT_co, _VT_co]]: ...

View File

@@ -35,8 +35,8 @@ from urllib3.fields import RequestField
from urllib3.filepost import encode_multipart_formdata
from urllib3.util import parse_url
from . import _types as _t
from ._internal_utils import to_native_string, unicode_is_ascii
from ._types import SupportsRead as _SupportsRead
from .auth import HTTPBasicAuth
from .compat import (
JSONDecodeError,
@@ -87,7 +87,6 @@ if TYPE_CHECKING:
from typing_extensions import Self
from . import _types as _t
from .adapters import HTTPAdapter
from .cookies import RequestsCookieJar
@@ -161,7 +160,7 @@ class RequestEncodingMixin:
if isinstance(data, (str, bytes)):
return data
elif isinstance(data, _SupportsRead):
elif _t.has_read(data):
return data
elif hasattr(data, "__iter__"):
result: list[tuple[bytes, bytes]] = []
@@ -236,9 +235,7 @@ class RequestEncodingMixin:
if isinstance(fp, (str, bytes, bytearray)):
fdata = fp
# data that proxies attributes to underlying objects needs hasattr
# defensive check for untyped callers
elif isinstance(fp, _SupportsRead) or hasattr(fp, "read"):
elif _t.has_read(fp):
fdata = fp.read()
elif fp is None: # defensive check for untyped callers
continue
@@ -641,7 +638,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
else:
if raw_data:
body = self._encode_params(raw_data)
if isinstance(data, basestring) or isinstance(data, _SupportsRead):
if isinstance(data, basestring) or _t.has_read(data):
content_type = None
else:
content_type = "application/x-www-form-urlencoded"

View File

@@ -1098,6 +1098,21 @@ class TestRequests:
assert r.status_code == 200
assert r.json()["files"]["file"] == "named temp file contents\n"
def test_post_getattr_proxy_read_only(self, httpbin):
class ReadProxy:
def __init__(self):
self._file = io.BytesIO(b"streamed body")
def __getattr__(self, name):
if name == "__iter__":
raise AttributeError(name)
return getattr(self._file, name)
r = requests.post(httpbin("post"), data=ReadProxy())
assert r.status_code == 200
assert r.json()["data"] == "streamed body"
@pytest.mark.parametrize(
"data",
(