Skip to content

Commit 8561a35

Browse files
committed
Add hasattr checks for remaining protocol isinstance checks
1 parent 6f205ff commit 8561a35

3 files changed

Lines changed: 24 additions & 7 deletions

File tree

src/requests/_types.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ class SupportsRead(Protocol[_T_co]):
2929
def read(self, length: int = ..., /) -> _T_co: ...
3030

3131

32+
def has_read(obj: Any) -> TypeIs[SupportsRead[str | bytes]]:
33+
"""Check if obj supports read, including __getattr__ based proxies."""
34+
return isinstance(obj, SupportsRead) or hasattr(obj, "read")
35+
36+
3237
@runtime_checkable
3338
class SupportsItems(Protocol[_KT_co, _VT_co]):
3439
def items(self) -> Iterable[tuple[_KT_co, _VT_co]]: ...

src/requests/models.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@
3535
from urllib3.filepost import encode_multipart_formdata
3636
from urllib3.util import parse_url
3737

38+
from . import _types as _t
3839
from ._internal_utils import to_native_string, unicode_is_ascii
39-
from ._types import SupportsRead as _SupportsRead
4040
from .auth import HTTPBasicAuth
4141
from .compat import (
4242
JSONDecodeError,
@@ -87,7 +87,6 @@
8787

8888
from typing_extensions import Self
8989

90-
from . import _types as _t
9190
from .adapters import HTTPAdapter
9291
from .cookies import RequestsCookieJar
9392

@@ -161,7 +160,7 @@ def _encode_params(
161160

162161
if isinstance(data, (str, bytes)):
163162
return data
164-
elif isinstance(data, _SupportsRead):
163+
elif _t.has_read(data):
165164
return data
166165
elif hasattr(data, "__iter__"):
167166
result: list[tuple[bytes, bytes]] = []
@@ -236,9 +235,7 @@ def _encode_files(
236235

237236
if isinstance(fp, (str, bytes, bytearray)):
238237
fdata = fp
239-
# data that proxies attributes to underlying objects needs hasattr
240-
# defensive check for untyped callers
241-
elif isinstance(fp, _SupportsRead) or hasattr(fp, "read"):
238+
elif _t.has_read(fp):
242239
fdata = fp.read()
243240
elif fp is None: # defensive check for untyped callers
244241
continue
@@ -641,7 +638,7 @@ def prepare_body(
641638
else:
642639
if raw_data:
643640
body = self._encode_params(raw_data)
644-
if isinstance(data, basestring) or isinstance(data, _SupportsRead):
641+
if isinstance(data, basestring) or _t.has_read(data):
645642
content_type = None
646643
else:
647644
content_type = "application/x-www-form-urlencoded"

tests/test_requests.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,6 +1098,21 @@ def test_post_named_tempfile(self, httpbin):
10981098
assert r.status_code == 200
10991099
assert r.json()["files"]["file"] == "named temp file contents\n"
11001100

1101+
def test_post_getattr_proxy_read_only(self, httpbin):
1102+
1103+
class ReadProxy:
1104+
def __init__(self):
1105+
self._file = io.BytesIO(b"streamed body")
1106+
1107+
def __getattr__(self, name):
1108+
if name == "__iter__":
1109+
raise AttributeError(name)
1110+
return getattr(self._file, name)
1111+
1112+
r = requests.post(httpbin("post"), data=ReadProxy())
1113+
assert r.status_code == 200
1114+
assert r.json()["data"] == "streamed body"
1115+
11011116
@pytest.mark.parametrize(
11021117
"data",
11031118
(

0 commit comments

Comments
 (0)