|
81 | 81 | HAS_PYOPENSSL = False |
82 | 82 |
|
83 | 83 |
|
| 84 | +class AttrProxyFileWrapper: |
| 85 | + """File wrapper that proxies all attribute access via __getattr__. |
| 86 | +
|
| 87 | + Mimics tqdm.utils.CallbackIOWrapper/ObjectWrapper. Has read/tell/seek |
| 88 | + via the underlying file, but does NOT define __iter__ in its own class, |
| 89 | + so isinstance(..., Iterable) returns False while hasattr("read") is True. |
| 90 | + """ |
| 91 | + |
| 92 | + def __init__(self, fileobj): |
| 93 | + self._fileobj = fileobj |
| 94 | + |
| 95 | + def __getattr__(self, name): |
| 96 | + return getattr(self._fileobj, name) |
| 97 | + |
| 98 | + |
84 | 99 | class TestRequests: |
85 | 100 | digest_auth_algo = ("MD5", "SHA-256", "SHA-512") |
86 | 101 |
|
@@ -2073,6 +2088,82 @@ def __iter__(self): |
2073 | 2088 |
|
2074 | 2089 | assert "Unable to rewind request body" in str(e) |
2075 | 2090 |
|
| 2091 | + def test_attr_proxy_body_position_is_recorded(self): |
| 2092 | + data = AttrProxyFileWrapper(io.BytesIO(b"the data")) |
| 2093 | + prep = requests.Request("GET", "http://example.com", data=data).prepare() |
| 2094 | + assert prep._body_position == 0 |
| 2095 | + assert prep.body is data |
| 2096 | + |
| 2097 | + def test_attr_proxy_content_length_is_set(self): |
| 2098 | + data = AttrProxyFileWrapper(io.BytesIO(b"the data")) |
| 2099 | + prep = requests.Request("GET", "http://example.com", data=data).prepare() |
| 2100 | + assert prep.headers["Content-Length"] == "8" |
| 2101 | + |
| 2102 | + def test_rewind_attr_proxy_body(self): |
| 2103 | + data = AttrProxyFileWrapper(io.BytesIO(b"the data")) |
| 2104 | + prep = requests.Request("GET", "http://example.com", data=data).prepare() |
| 2105 | + assert prep._body_position == 0 |
| 2106 | + |
| 2107 | + assert prep.body.read() == b"the data" |
| 2108 | + assert prep.body.read() == b"" |
| 2109 | + |
| 2110 | + requests.utils.rewind_body(prep) |
| 2111 | + assert prep.body.read() == b"the data" |
| 2112 | + |
| 2113 | + def test_rewind_partially_read_attr_proxy_body(self): |
| 2114 | + raw = io.BytesIO(b"the data") |
| 2115 | + raw.read(4) |
| 2116 | + data = AttrProxyFileWrapper(raw) |
| 2117 | + prep = requests.Request("GET", "http://example.com", data=data).prepare() |
| 2118 | + assert prep._body_position == 4 |
| 2119 | + |
| 2120 | + assert prep.body.read() == b"data" |
| 2121 | + |
| 2122 | + requests.utils.rewind_body(prep) |
| 2123 | + assert prep.body.read() == b"data" |
| 2124 | + |
| 2125 | + def test_rewind_attr_proxy_body_no_seek(self): |
| 2126 | + class NoSeekWrapper: |
| 2127 | + def __init__(self): |
| 2128 | + pass |
| 2129 | + |
| 2130 | + def tell(self): |
| 2131 | + return 0 |
| 2132 | + |
| 2133 | + def __iter__(self): |
| 2134 | + return iter([]) |
| 2135 | + |
| 2136 | + data = NoSeekWrapper() |
| 2137 | + prep = requests.Request("GET", "http://example.com", data=data).prepare() |
| 2138 | + assert prep._body_position == 0 |
| 2139 | + |
| 2140 | + with pytest.raises(UnrewindableBodyError): |
| 2141 | + requests.utils.rewind_body(prep) |
| 2142 | + |
| 2143 | + def test_read_only_object_detected_as_stream(self): |
| 2144 | + """An object with read() but no __iter__ should still be a stream.""" |
| 2145 | + |
| 2146 | + class ReadOnlyFile: |
| 2147 | + def __init__(self, data): |
| 2148 | + self._buf = io.BytesIO(data) |
| 2149 | + |
| 2150 | + def read(self, n=-1): |
| 2151 | + return self._buf.read(n) |
| 2152 | + |
| 2153 | + def tell(self): |
| 2154 | + return self._buf.tell() |
| 2155 | + |
| 2156 | + def seek(self, pos): |
| 2157 | + return self._buf.seek(pos) |
| 2158 | + |
| 2159 | + def __len__(self): |
| 2160 | + return self._buf.getbuffer().nbytes |
| 2161 | + |
| 2162 | + data = ReadOnlyFile(b"the data") |
| 2163 | + prep = requests.Request("GET", "http://example.com", data=data).prepare() |
| 2164 | + assert prep._body_position == 0 |
| 2165 | + assert prep.body is data |
| 2166 | + |
2076 | 2167 | def _patch_adapter_gzipped_redirect(self, session, url): |
2077 | 2168 | adapter = session.get_adapter(url=url) |
2078 | 2169 | org_build_response = adapter.build_response |
|
0 commit comments