Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ jobs:
run: |
uv pip install .[test]
uv pip install maturin
# uvloop is optional and can lag a new Python release; never build it from source.
uv pip install --only-binary uvloop uvloop || echo "::warning::no uvloop wheel; skipping uvloop tests"
- name: Build wheels
run: |
source .venv/bin/activate
Expand Down Expand Up @@ -366,7 +368,8 @@ jobs:
timeout-minutes: 10
run: |
python -m pip install --no-index --find-links dist wreq
python -m pip install pytest pytest-asyncio uvloop
python -m pip install pytest pytest-asyncio
python -m pip install --only-binary uvloop uvloop || echo "::warning::no uvloop wheel; skipping uvloop tests"
python -m pytest -v -o faulthandler_timeout=60 tests/aio_test.py tests/cancellation_test.py tests/upload_test.py tests/runtime_test.py
- name: Upload wheels
uses: actions/upload-artifact@v7
Expand Down
3 changes: 1 addition & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,8 @@ pyo3 = { version = "0.29.0", features = [
"multiple-pymethods",
"extension-module",
"generate-import-lib",
"experimental-async",
] }
futures-util = { version = "0.3.33", default-features = false }
futures-util = { version = "0.3.33", default-features = false, features = ["std"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1"
serde_urlencoded = "0.7.1"
Expand Down Expand Up @@ -84,6 +83,8 @@ wreq = { git = "https://github.com/0x676e67/wreq", rev = "54fdc7c9e0c1290e2f70f3
btls = { git = "https://github.com/0x676e67/btls", rev = "ffaf0e8c23d455af4e306986ce81bb57ebd03bc2" }
btls-sys = { git = "https://github.com/0x676e67/btls", rev = "ffaf0e8c23d455af4e306986ce81bb57ebd03bc2" }
tokio-btls = { git = "https://github.com/0x676e67/btls", rev = "ffaf0e8c23d455af4e306986ce81bb57ebd03bc2" }
# main (0x676e67/netty#145)
netty = { git = "https://github.com/0x676e67/netty", rev = "f8652ddf5e386d66b7f03cceedbc6b319d849762" }

[profile.release]
codegen-units = 1
Expand Down
6 changes: 3 additions & 3 deletions docs/source/getting-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ print(data)
```python
import hashlib

view = await response.bytes()
await response.close()
print(view.readonly) # True; closing the response does not invalidate the view
async with response:
view = await response.bytes()
print(view.readonly) # True; releasing the response does not invalidate the view
print(hashlib.sha256(view).hexdigest()) # Reads the buffer directly
```

Expand Down
2 changes: 1 addition & 1 deletion docs/source/guide/basic.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,6 @@ async def main():

Data chunks are read-only `memoryview` objects; trailer frames are `HeaderMap` objects. The example writes data directly through the buffer protocol. Each view stays valid after the stream is closed. See [Binary data](../getting-started/quickstart.md#binary-data) for copying and releasing views.

Finish a body read before closing the response. If a read task is still running, cancel it and await its cancellation first: `response.close()` does not cancel active reads or guarantee an immediate socket shutdown. `response.stream()` transfers the body to the streamer, so manage the streamer with its own context manager as shown above.
Finish a body read before closing the response. If a read task is still running, cancel it and await its cancellation first: `response.close()` does not cancel active reads or guarantee an immediate socket shutdown. It also marks the connection as non-reusable; leaving `async with response:` instead releases the body and keeps a fully read connection reusable. `response.stream()` transfers the body to the streamer, so manage the streamer with its own context manager as shown above.

---
2 changes: 1 addition & 1 deletion docs/source/guide/blocking.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,4 +210,4 @@ if __name__ == "__main__":

Data chunks are read-only `memoryview` objects that stay valid after the stream is closed. `resp.bytes()` returns the same type. Pass views directly to APIs that accept the buffer protocol; use `bytes(view)` or `view.tobytes()` only when you need a copy.

Do not close a response while another thread is reading its body. `resp.close()` discards the retained body and marks its connection as non-reusable, but does not guarantee an immediate socket shutdown or interrupt an active read. A body transferred by `resp.stream()` belongs to the streamer and needs its own context manager, as shown above.
Do not close a response while another thread is reading its body. `resp.close()` discards the retained body and marks its connection as non-reusable, but does not guarantee an immediate socket shutdown or interrupt an active read. Leaving `with resp:` instead keeps a fully read connection reusable. A body transferred by `resp.stream()` belongs to the streamer and needs its own context manager, as shown above.
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ test = [
"pytest",
"pytest-asyncio",
"pytest-rerunfailures",
"uvloop; sys_platform != 'win32' and platform_python_implementation == 'CPython'",
]
docs = [
"zensical",
Expand Down
5 changes: 3 additions & 2 deletions python/wreq/_compat.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Compatibility for PyPy's legacy exception delegation to PyO3 coroutines."""
"""Compatibility for PyPy's legacy exception delegation to wreq coroutines."""

from types import TracebackType

Expand All @@ -8,7 +8,8 @@ def _install(coroutine_type):
if getattr(original, "_wreq_pypy_throw_compat", False):
return

# Remove this shim once PyO3 accepts throw(type, value, traceback).
# Coroutine.throw takes one exception, as asyncio passes; PyPy delegates
# throws from awaiting generators as the legacy (type, value, traceback).
def throw(self, exc, value=None, traceback=None):
if traceback is not None and type(traceback) is not TracebackType:
raise TypeError("throw() third argument must be a traceback object")
Expand Down
18 changes: 15 additions & 3 deletions python/wreq/blocking.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,12 @@ def close(self) -> None:
"""

def __enter__(self) -> Any: ...
def __exit__(self, _exc_type: Any, _exc_value: Any, _traceback: Any) -> None: ...
def __exit__(self, _exc_type: Any, _exc_value: Any, _traceback: Any) -> None:
r"""
Release the body without forbidding reuse: a fully read connection returns
to the pool, while an unread HTTP/1 body drains or closes its connection.
"""

def __str__(self) -> str: ...


Expand Down Expand Up @@ -205,7 +210,11 @@ def close(
"""

def __enter__(self) -> Any: ...
def __exit__(self, _exc_type: Any, _exc_value: Any, _traceback: Any) -> None: ...
def __exit__(self, _exc_type: Any, _exc_value: Any, _traceback: Any) -> None:
r"""
Close the WebSocket connection without a close code or reason, unless already closed.
"""

def __str__(self) -> str: ...


Expand Down Expand Up @@ -489,7 +498,10 @@ def get(
...

def __enter__(self) -> Any: ...
def __exit__(self, _exc_type: Any, _exc_value: Any, _traceback: Any) -> None: ...
def __exit__(self, _exc_type: Any, _exc_value: Any, _traceback: Any) -> None:
r"""
Close the client like `close()`: cancel pending requests and reject new ones.
"""


def delete(
Expand Down
37 changes: 24 additions & 13 deletions python/wreq/wreq.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,17 +318,21 @@ def __next__(self) -> memoryview | HeaderMap: ...

def __enter__(self) -> Any: ...

def __exit__(self, _exc_type: Any, _exc_value: Any, _traceback: Any) -> None: ...
def __exit__(self, _exc_type: Any, _exc_value: Any, _traceback: Any) -> None:
r"""
Release the body and end any pending read; returned views stay valid.
"""

def __aiter__(self) -> "Streamer": ...

async def __anext__(self) -> memoryview | HeaderMap: ...

async def __aenter__(self) -> Any: ...

async def __aexit__(
self, _exc_type: Any, _exc_value: Any, _traceback: Any
) -> None: ...
async def __aexit__(self, _exc_type: Any, _exc_value: Any, _traceback: Any) -> None:
r"""
Release the body and end any pending read; returned views stay valid.
"""


class Response:
Expand All @@ -348,7 +352,6 @@ async def main():
print("Response URL: ", response.url)
print("Headers: ", response.headers)
print("Content-Length: ", response.content_length)
print("Encoding: ", response.encoding)
print("Remote Address: ", response.remote_addr)

text_content = await response.text()
Expand Down Expand Up @@ -449,9 +452,12 @@ async def close(self) -> None:
"""

async def __aenter__(self) -> Any: ...
async def __aexit__(
self, _exc_type: Any, _exc_value: Any, _traceback: Any
) -> Any: ...
async def __aexit__(self, _exc_type: Any, _exc_value: Any, _traceback: Any) -> Any:
r"""
Release the body without forbidding reuse: a fully read connection returns
to the pool, while an unread HTTP/1 body drains or closes its connection.
"""

def __str__(self) -> str: ...


Expand Down Expand Up @@ -519,8 +525,12 @@ async def close(
Close the WebSocket connection.
"""

def __aenter__(self) -> Any: ...
def __aexit__(self, _exc_type: Any, _exc_value: Any, _traceback: Any) -> Any: ...
async def __aenter__(self) -> Any: ...
async def __aexit__(self, _exc_type: Any, _exc_value: Any, _traceback: Any) -> Any:
r"""
Close the WebSocket connection without a close code or reason, unless already closed.
"""

def __str__(self) -> str: ...


Expand Down Expand Up @@ -1433,9 +1443,10 @@ async def main():
...

async def __aenter__(self) -> Any: ...
async def __aexit__(
self, _exc_type: Any, _exc_value: Any, _traceback: Any
) -> Any: ...
async def __aexit__(self, _exc_type: Any, _exc_value: Any, _traceback: Any) -> Any:
r"""
Close the client like `close()`: cancel pending requests and reject new ones.
"""


async def delete(
Expand Down
12 changes: 7 additions & 5 deletions src/aio.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
//! Awaitables that resume asyncio tasks without attaching Tokio threads to Python.
//! Awaitables that drive Rust futures from asyncio tasks.
//!
//! Rust futures complete on Tokio workers, which never take the GIL. A wake marks
//! its [`Coroutine`] ready and queues it on the [`Port`] of the awaiting event loop,
//! ringing a socket the loop polls only when the queue was empty. The loop thread
//! then resolves every queued asyncio future in one batch.
//! A [`Coroutine`] polls its future on the event loop thread, and [`spawn`] moves
//! the work to Tokio. A wake marks the coroutine ready and queues it on the [`Port`]
//! of the awaiting loop, ringing a socket the loop watches only when the queue was
//! empty, so waking Tokio threads never take the GIL. A loop that cannot watch the
//! socket is woken with `call_soon_threadsafe`, which does attach the waking thread.
//! The loop thread then resolves every queued asyncio future in one batch.

mod coroutine;
mod port;
Expand Down
Loading
Loading