Skip to content
Open
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 Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ serde_json = "1"
serde_urlencoded = "0.7.1"
indexmap = { version = "2.13.0", features = ["serde"] }
bytes = "1.12.1"
memchr = "2.8"
http = "1.4.2"
http-body = "1.1.0"
http-body-util = "0.1.4"
Expand Down
2 changes: 2 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
- Unknown keyword arguments now raise `TypeError` when calling async or blocking client constructors, HTTP or WebSocket request methods and functions, `Http1Options`, `Http2Options`, `TlsOptions`, or `Proxy` factories. These arguments were previously ignored.
- Set client-only options such as `connect_timeout` and `proxies` on `Client`; passing them to a request now raises `TypeError`. Use `proxy` to select a proxy for one request.
- Async request calls check keyword names before returning a coroutine. Values for supported request keywords are still converted on first `await` or entry with `async with`.
- Header names and values, `Message.ping`/`pong` and `TlsInfo.peer_certificate()` return `bytes` again instead of `memoryview`. `Response.bytes()`, stream data frames and `Message.data`/`binary` return the new immutable `wreq.Bytes`, which shares data without copying and compares and hashes like `bytes`. Tell stream frames apart with `isinstance(chunk, HeaderMap)` rather than `isinstance(chunk, memoryview)`, and use `memoryview(data)` for `memoryview` attributes such as `.obj`, `.cast()` or `.tolist()`. `bytes` values have no `.tobytes()` or `.release()`; on `wreq.Bytes`, `release()` and `with` emit `DeprecationWarning`. APIs that accept only `bytes`, `bytearray` or `memoryview` by type, such as asyncio `StreamWriter.write` and transports or `orjson.loads`, reject `wreq.Bytes`; pass `memoryview(data)`, which does not copy.
- `memoryview` and `wreq.Bytes` values are now accepted wherever `bytes` is, and are sent as raw bytes. `body=` and `Message.from_binary()` previously JSON-encoded a `memoryview` as an array of integers.

## [0.11.0](https://github.com/0x676e67/wreq-python/compare/v0.10.2..v0.11.0) - 2026-04-13

Expand Down
6 changes: 6 additions & 0 deletions docs/source/api/wreq.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ The main `wreq` module contains core classes and types used throughout the libra
options:
show_root_heading: true

## Binary Data

::: wreq.Bytes
options:
show_root_heading: true

## Network Types

::: wreq.SocketAddr
Expand Down
13 changes: 8 additions & 5 deletions docs/source/getting-started/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,14 @@ connections, and use context managers to close resources when you're done.

- **Streaming and buffers**

Stream request and response bodies, or read a complete body. Rust-backed
binary outputs are read-only `memoryview` objects. Convert to `bytes` when
an API requires that type; the conversion copies the data.

[Advanced features](../guide/advanced.md)
Stream request and response bodies, or read a complete body. Bodies and
stream data are zero-copy `wreq.Bytes` objects, and short values such as
headers are `bytes`. Use `data.tobytes()` when an API requires `bytes`; the
conversion copies the data.

[Streaming responses](../guide/basic.md#streaming-responses) ·
[Uploads](../guide/advanced.md#streaming-request-bodies) ·
[Binary data](quickstart.md#binary-data)

- **Runtime configuration**

Expand Down
40 changes: 22 additions & 18 deletions docs/source/getting-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,32 +108,36 @@ Finish each read before starting another. For large bodies, see

### Binary data

`bytes()` returns a read-only `memoryview` backed by Rust-owned data. Returning
the view is zero-copy; collecting a complete response can still allocate memory
to join body chunks.
`bytes()` returns an immutable `wreq.Bytes` backed by Rust-owned data. Returning
it is zero-copy; collecting a complete response can still allocate memory to
join body chunks.

```python
import hashlib

async with client.get("https://httpbin.org/bytes/16") as response:
view = await response.bytes()
data = await response.bytes()

print(view.readonly) # True
print(hashlib.sha256(view).hexdigest())
print(len(data), data[:4])
print(hashlib.sha256(data).hexdigest())
```

The view remains valid after the response closes. Use it directly with APIs
that accept buffers, such as `file.write(view)` or `hashlib.sha256(view)`.
For text, use `str(view, "utf-8")`; memoryviews have no `.decode()` method.

Use `bytes(view)` or `view.tobytes()` when an API needs a `bytes` object. Both
copy the data. This includes passing a view back to wreq's binary inputs such
as `body`, `Part`, `Message` constructors or `CertStore`. You can release your
reference early with `view.release()`; other views and slices stay valid.

The blocking API returns the same type. Stream data frames, binary WebSocket
fields, header names and values, and peer certificates also use read-only
memoryviews that retain their backing data.
The data remains valid after the response closes. Use it directly with APIs
that accept buffers, such as `file.write(data)` or `hashlib.sha256(data)`. It
compares and hashes like `bytes`, and provides read-only `bytes` methods such as
`decode()`, `find()`, `count()` and `startswith()`, plus zero-copy slicing.
Passing it back to wreq, for example as a request `body` or to
`Message.from_binary`, does not copy it.

Use `data.tobytes()` when an API needs a `bytes` object, such as `json.loads`
or code that checks `isinstance(x, bytes)`; it copies the data. APIs that accept
only `bytes`, `bytearray` or `memoryview` by type, such as asyncio transports
and `orjson.loads`, take `memoryview(data)` without copying.

The blocking API returns the same type, and so do stream data frames and the
`data` and `binary` fields of WebSocket messages. Short values are plain
`bytes`: header names and values, WebSocket ping and pong payloads, and peer
certificates.

## Set headers

Expand Down
5 changes: 3 additions & 2 deletions docs/source/guide/advanced.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# Advanced features

Use streaming uploads for data produced incrementally, and multipart forms for
named fields and files. Response streaming is covered in [basic usage](basic.md).
named fields and files. Response streaming is covered in
[basic usage](basic.md#streaming-responses).

## Streaming request bodies

Pass a synchronous iterator or an async generator to `body`. Each item must
be a supported body chunk, such as `bytes` or `str`:
be `str` or bytes-like data, such as `bytes` or `wreq.Bytes`:

```python
import asyncio
Expand Down
28 changes: 14 additions & 14 deletions docs/source/guide/basic.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,9 @@ keywords are converted when the coroutine is first awaited or entered with
## Request bodies

Use `json` for JSON, `form` for URL-encoded fields, and `body` for raw text or
bytes. `json` and `form` set their content type automatically. Set it explicitly
for a raw body when the server needs it. Choose one body argument per request.
bytes-like data (`bytes`, `bytearray`, `memoryview` or `wreq.Bytes`). `json` and
`form` set their content type automatically. Set it explicitly for a raw body
when the server needs it. Choose one body argument per request.

```python
async with client.post(
Expand Down Expand Up @@ -117,18 +118,17 @@ from wreq.header import HeaderMap

headers = HeaderMap({"Accept": "application/json"})
headers.append("Accept", "text/html")
print([str(value, "ascii") for value in headers.get_all("accept")])
print([value.decode("ascii") for value in headers.get_all("accept")])

async with client.get("https://httpbin.org/headers", headers=headers) as response:
content_type = response.headers.get("content-type")
if content_type is not None:
print(str(content_type, "ascii"))
print(content_type.decode("ascii"))
print(await response.json())
```

Header names are case insensitive. Header names and values returned by
`HeaderMap` are read-only memoryviews; `str(view, encoding)` decodes them
without an intermediate `bytes` copy.
`HeaderMap` are `bytes`.

## Authentication

Expand Down Expand Up @@ -209,10 +209,10 @@ another. `stream()` only accepts an unread body and transfers it to a streamer;
after that, use the streamer to read it. Reading a released body or starting an
overlapping read raises `RuntimeError`.

`bytes()` returns a zero-copy, read-only memoryview of the collected Rust
buffer. Collecting the body may allocate to join chunks. Views stay valid after
the response closes; see [binary data](../getting-started/quickstart.md#binary-data)
for decoding, copying and releasing them.
`bytes()` returns a zero-copy `wreq.Bytes` of the collected Rust buffer.
Collecting the body may allocate to join chunks. The data stays valid after the
response closes; see [binary data](../getting-started/quickstart.md#binary-data)
for decoding and copying it.

## Concurrent requests

Expand Down Expand Up @@ -253,7 +253,7 @@ propagate when a caller cancels your task.
## Streaming responses

Read large bodies incrementally with `response.stream()`. Data frames are
read-only memoryviews, and trailing HTTP headers are `HeaderMap` objects.
`wreq.Bytes`, and trailing HTTP headers are `HeaderMap` objects.
This complete example counts the bytes without retaining the whole body:

```python
Expand All @@ -269,10 +269,10 @@ async def main():
response.raise_for_status()
async with response.stream() as streamer:
async for frame in streamer:
if isinstance(frame, memoryview):
total += len(frame)
elif isinstance(frame, HeaderMap):
if isinstance(frame, HeaderMap):
print("Trailers:", frame)
else:
total += len(frame)
print("Received bytes:", total)


Expand Down
12 changes: 6 additions & 6 deletions docs/source/guide/blocking.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ Closing a client cancels pending requests and rejects new ones with

## Streaming response

`response.bytes()` returns a read-only memoryview of the complete body. To
`response.bytes()` returns a zero-copy `wreq.Bytes` of the complete body. To
write a large response incrementally, use a streamer:

```python
Expand All @@ -124,14 +124,14 @@ with Client() as client:
response.raise_for_status()
with response.stream() as streamer, open("download.bin", "wb") as output:
for frame in streamer:
if isinstance(frame, memoryview):
output.write(frame)
elif isinstance(frame, HeaderMap):
if isinstance(frame, HeaderMap):
print("Trailers:", frame)
else:
output.write(frame)
```

Data views remain valid after the stream closes. `output.write(frame)` accepts
the buffer directly; `bytes(frame)` makes a copy. Only an unread response can
Data frames remain valid after the stream closes. `output.write(frame)` accepts
the buffer directly; `frame.tobytes()` makes a copy. Only an unread response can
be streamed. Complete body reads through `text()`, `json()` or `bytes()` are
cached and can be repeated sequentially until the response is released.

Expand Down
2 changes: 1 addition & 1 deletion docs/source/guide/redirect-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ async def main():
print(response.status)
location = response.headers.get("location")
if location is not None:
print("Location:", bytes(location).decode())
print("Location:", location.decode())
await response.bytes()


Expand Down
9 changes: 5 additions & 4 deletions docs/source/guide/websocket.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ Use `Message.from_binary(data)` to send binary data. `send_all([...])` sends
a sequence of messages in order. To choose the closing status, call
`await ws.close(code=1000, reason="Finished")` before leaving the context.

Binary payload access is zero-copy: `Message.data`, `.binary`, `.ping`, and
`.pong` return read-only `memoryview` objects when present. The views remain
valid after the message is deleted or the connection closes. Use `bytes(view)`
when another API requires a `bytes` object; `Message.text` returns a string.
Binary payload access is zero-copy: `Message.data` and `.binary` return
`wreq.Bytes` when present, which stay valid after the message is deleted or the
connection closes. `.ping` and `.pong` return `bytes`, since RFC 6455 limits
control payloads to 125 bytes. Use `data.tobytes()` when another API requires a
`bytes` object; `Message.text` returns a string.

## Receive timeouts

Expand Down
2 changes: 1 addition & 1 deletion docs/source/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ asyncio.run(main())

<a class="wreq-feature" href="benchmark/"><span class="wreq-feature-label">02 · PERFORMANCE</span><h3>High-throughput HTTPS</h3><p>Rust handles the network I/O. See how wreq compares with other Python clients, then choose the body size and runtime mode that matter to your workload.</p><span class="wreq-feature-link">View performance results</span></a>

<a class="wreq-feature" href="guide/advanced/"><span class="wreq-feature-label">03 · STREAMING</span><h3>Stream uploads and responses</h3><p>Send an upload in chunks or read a response as it arrives. Response chunks are read-only memoryviews backed by Rust buffers.</p><span class="wreq-feature-link">Streaming guide</span></a>
<a class="wreq-feature" href="guide/basic/#streaming-responses"><span class="wreq-feature-label">03 · STREAMING</span><h3>Stream uploads and responses</h3><p>Send an upload in chunks or read a response as it arrives. Response chunks are zero-copy wreq.Bytes objects backed by Rust buffers.</p><span class="wreq-feature-link">Streaming guide</span></a>

<a class="wreq-feature" href="guide/blocking/"><span class="wreq-feature-label">04 · PYTHON</span><h3>Async and blocking APIs</h3><p>Use await in async code. For a synchronous script, import Client from wreq.blocking.</p><span class="wreq-feature-link">Blocking API</span></a>

Expand Down
6 changes: 3 additions & 3 deletions examples/blocking/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ def main():
with wreq.blocking.get("https://httpbin.io/stream/20") as resp:
with resp.stream() as streamer:
for chunk in streamer:
if isinstance(chunk, memoryview):
sys.stdout.buffer.write(chunk)
else:
if isinstance(chunk, wreq.HeaderMap):
print("Trailers:", chunk)
else:
sys.stdout.buffer.write(chunk)
time.sleep(0.1)


Expand Down
26 changes: 26 additions & 0 deletions examples/bytes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import asyncio
import hashlib
import json

import wreq


async def main():
async with wreq.Client() as client:
async with client.get("https://httpbin.io/json") as resp:
# An immutable wreq.Bytes that shares the body without copying.
data = await resp.bytes()

# Buffer APIs read it in place, and slices share it too.
print(hashlib.sha256(data).hexdigest(), data[:16])

# json.loads() takes only str, bytes or bytearray.
print(json.loads(data.decode())["slideshow"]["title"])

# Passing it back as a request body does not copy it.
async with client.post("https://httpbin.io/anything", body=data) as resp:
print(resp.status)


if __name__ == "__main__":
asyncio.run(main())
6 changes: 3 additions & 3 deletions examples/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ async def main():
async with wreq.get("https://httpbin.io/stream/20") as resp:
async with resp.stream() as streamer:
async for chunk in streamer:
if isinstance(chunk, memoryview):
sys.stdout.buffer.write(chunk)
else:
if isinstance(chunk, wreq.HeaderMap):
print("Trailers:", chunk)
else:
sys.stdout.buffer.write(chunk)
await asyncio.sleep(0.1)


Expand Down
5 changes: 5 additions & 0 deletions python/wreq/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# wreq/__init__.py

from collections.abc import Sequence as _Sequence

from .wreq import *

from .cookie import *
Expand All @@ -14,6 +16,9 @@
from .proxy import *
from .runtime import *

# A Sequence like `bytes`; like `memoryview`, it also matches sequence patterns in `match`.
_Sequence.register(Bytes) # pyright: ignore[reportAttributeAccessIssue]

__all__ = (
header.__all__
+ cookie.__all__
Expand Down
14 changes: 8 additions & 6 deletions python/wreq/blocking.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from .redirect import History
from .runtime import Runtime
from .tls import TlsInfo
from .wreq import Bytes


class Response:
Expand Down Expand Up @@ -86,7 +87,7 @@ def raise_for_status(self) -> None:

def stream(self) -> Streamer:
r"""
Stream the body as read-only memoryviews, with HeaderMap frames for trailers.
Stream the body as immutable `Bytes`, with HeaderMap frames for trailers.
"""
...

Expand All @@ -101,11 +102,12 @@ def json(self) -> Any:
Get the JSON content of the response.
"""

def bytes(self) -> memoryview:
def bytes(self) -> Bytes:
r"""
Read the body as a read-only memoryview without copying it into Python bytes.
The view remains valid after the response is closed or deleted.
Use bytes(view) or view.tobytes() for a copy; view.release() releases this view.
Read the body as an immutable `Bytes` without copying it.
The data stays valid after the response is closed or deleted.
Use `data.tobytes()` for a `bytes` copy, or `memoryview(data)` for APIs that accept
only `bytes`, `bytearray` or `memoryview`.
"""
...

Expand All @@ -114,7 +116,7 @@ def close(self) -> None:
Discard the retained body and mark its connection as non-reusable.
This does not guarantee an immediate socket shutdown or interrupt an active read.
Do not close concurrently with a body read. A body transferred to a Streamer
is managed separately; previously returned memoryviews remain valid.
is managed separately; previously returned `Bytes` remain valid.
`with` instead releases the body and keeps a fully read connection reusable.
"""

Expand Down
Loading
Loading