Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2396,6 +2396,7 @@ async fn convert_async_response(
url,
request,
encoding,
history: Vec::new(),
default_encoding,
extensions: None,
stream: None,
Expand Down
29 changes: 28 additions & 1 deletion src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,19 @@ pub fn parse_default_encoding_arg(
))
}

fn parse_history_arg(py: Python<'_>, history: Option<Py<PyAny>>) -> PyResult<Vec<Py<PyResponse>>> {
let Some(history) = history else {
return Ok(Vec::new());
};
let bound = history.bind(py);
if bound.is_none() {
return Ok(Vec::new());
}
bound
.extract::<Vec<Py<PyResponse>>>()
.map_err(|_| PyTypeError::new_err("history must be a list of Response objects or None"))
}

fn immediate_awaitable<'py>(py: Python<'py>, value: Py<PyAny>) -> PyResult<Bound<'py, PyAny>> {
pyo3_async_runtimes::tokio::future_into_py(py, async move { Ok(value) })
}
Expand Down Expand Up @@ -758,6 +771,7 @@ pub struct PyResponse {
pub url: String,
pub request: Option<Py<PyRequest>>,
pub encoding: Option<String>,
pub history: Vec<Py<PyResponse>>,
pub default_encoding: Option<Py<PyAny>>,
pub extensions: Option<Py<PyAny>>,
// Streaming: Some(response) while stream is unread, None after read.
Expand Down Expand Up @@ -890,6 +904,7 @@ impl PyResponse {
url,
request,
encoding,
history: Vec::new(),
default_encoding,
extensions: None,
stream: None,
Expand Down Expand Up @@ -921,6 +936,7 @@ impl PyResponse {
url,
request,
encoding,
history: Vec::new(),
default_encoding,
extensions: None,
stream: Some(Arc::new(Mutex::new(Some(ResponseStream::Blocking(resp))))),
Expand Down Expand Up @@ -958,6 +974,7 @@ impl PyResponse {
url,
request,
encoding,
history: Vec::new(),
default_encoding,
extensions: None,
stream: None,
Expand Down Expand Up @@ -990,6 +1007,7 @@ impl PyResponse {
url,
request,
encoding,
history: Vec::new(),
default_encoding,
extensions: None,
stream: Some(Arc::new(Mutex::new(Some(ResponseStream::Async(resp))))),
Expand Down Expand Up @@ -1036,7 +1054,7 @@ impl PyResponse {
default_encoding: Option<Py<PyAny>>,
http_version: Option<String>,
) -> PyResult<Self> {
let _ = history;
let history = parse_history_arg(py, history)?;
let default_encoding = parse_default_encoding_arg(py, default_encoding)?;
let reason_phrase = reqwest::StatusCode::from_u16(status_code)
.ok()
Expand Down Expand Up @@ -1118,6 +1136,7 @@ impl PyResponse {
url: String::new(),
request,
encoding,
history,
default_encoding,
extensions,
stream: None,
Expand Down Expand Up @@ -1174,6 +1193,14 @@ impl PyResponse {
self.encoding.as_deref()
}

#[getter]
pub fn history(&self, py: Python<'_>) -> Vec<Py<PyResponse>> {
self.history
.iter()
.map(|response| response.clone_ref(py))
.collect()
}

#[getter]
pub fn is_redirect(&self) -> bool {
self.status_code >= 300 && self.status_code < 400
Expand Down
30 changes: 30 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,36 @@ def test_text(self):
r = httprs.Response(200, content=b"Hello, world!")
assert r.text == "Hello, world!"

def test_history_defaults_to_empty_list(self):
r = httprs.Response(200, content=b"ok")
assert r.history == []

def test_history_accepts_response_list(self):
redirect = httprs.Response(
301,
headers={"location": "https://example.com/final"},
content=b"",
)
final = httprs.Response(200, content=b"ok", history=[redirect])
history = final.history
assert len(history) == 1
assert history[0].status_code == 301
assert history[0].headers["location"] == "https://example.com/final"

def test_history_copied_from_input_list(self):
redirect1 = httprs.Response(301, content=b"")
redirect2 = httprs.Response(302, content=b"")
history = [redirect1]
final = httprs.Response(200, content=b"ok", history=history)
history.append(redirect2)
assert [r.status_code for r in final.history] == [301]

def test_history_rejects_invalid_items(self):
with pytest.raises(
TypeError, match="history must be a list of Response objects"
):
httprs.Response(200, content=b"ok", history=["not-a-response"])

def test_text_uses_default_encoding_when_charset_missing(self):
r = httprs.Response(
200,
Expand Down