Skip to content

feat(multipart): allow reusing types other than stream iterators#571

Merged
0x676e67 merged 1 commit intomainfrom
multipart
Apr 14, 2026
Merged

feat(multipart): allow reusing types other than stream iterators#571
0x676e67 merged 1 commit intomainfrom
multipart

Conversation

@0x676e67
Copy link
Copy Markdown
Owner

@0x676e67 0x676e67 commented Apr 14, 2026

Summary by CodeRabbit

  • New Features

    • Multipart forms with clonable parts (text, bytes, file paths) can be reused across multiple HTTP requests.
    • Stream-based parts remain single-use when reusing multipart instances; attempting to reuse them raises an error.
  • Tests

    • Added async tests verifying reuse behavior for clonable parts and one-shot behavior for stream parts.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

📝 Walkthrough

Walkthrough

Multipart construction is deferred: Multipart now stores parts: Vec<Part> and optional form, while Part holds explicit metadata and a Value payload. multipart::Form is built on-demand during extraction; clonable values can be reused, streams are one‑shot.

Changes

Cohort / File(s) Summary
Multipart core refactor
src/client/body/multipart.rs
Converted Multipart from tuple to struct with form: Option<multipart::Form> and parts: Vec<Part>. Part now stores name, value: Option<Value>, filename, mime, length, headers. Multipart::new defers form assembly; FromPyObject builds multipart::Form on demand.
Request wiring update
src/client/req.rs
Switched multipart field usage in execute_request from form.0 to form.form when applying the multipart option to the request builder.
Tests: multipart reuse semantics
tests/multipart_test.py
New async tests verifying reuse behavior: clonable parts can be reused across requests, stream parts are one‑shot, reusing the same Part works for clonable values and fails for stream values. Adds assert_form_value() helper.

Sequence Diagram

sequenceDiagram
    participant Py as Python Code
    participant MW as Multipart (wrapper)
    participant PS as Part Storage
    participant FB as Form Builder
    participant HTTP as HTTP Client

    rect rgba(100,150,255,0.5)
    Note over Py,FB: Old flow (immediate construction)
    Py->>MW: Multipart::new(parts)
    MW->>FB: build multipart::Form immediately
    FB-->>MW: multipart::Form
    MW->>HTTP: send request with form
    end

    rect rgba(100,255,150,0.5)
    Note over Py,FB: New flow (deferred construction)
    Py->>MW: Multipart::new(parts)
    MW->>PS: store Part metadata & Value
    PS-->>MW: parts cached
    Py->>MW: reuse Multipart
    MW->>FB: FromPyObject triggers on-demand form build
    FB->>PS: iterate parts (clone or take values)
    FB-->>MW: assembled multipart::Form
    MW->>HTTP: send request with form
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I tuck each part in careful rows,
Deferred until the moment shows.
Text and bytes may hop and play,
Streams take off and run away —
Reuse with care, and off we go! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main change: refactoring multipart types to support reusing clonable parts (Text, Bytes, File) across multiple requests while restricting Stream parts to single-use.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch multipart

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the multipart form implementation to support form reuse by storing parts and their values (Text, Bytes, File, or Stream) within the Multipart and Part structs. This allows clonable parts to be reused across multiple requests while maintaining one-shot behavior for streams. Feedback was provided regarding the use of block_on when building file parts, as this can cause panics if called from within an existing Tokio runtime; it is recommended to make the form building process asynchronous.

Comment on lines +128 to +130
Value::File(path) => pyo3_async_runtimes::tokio::get_runtime()
.block_on(multipart::Part::file(path))
.map_err(Error::from)?,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using block_on inside build_inner is risky and can lead to panics if this code is executed on a thread that is already part of a Tokio runtime (e.g., the event loop thread). Since Multipart::extract is called during argument parsing for async functions like execute_request, it is highly likely to be called from such a context.

Consider making build_inner, into_inner, and build_form asynchronous. You can then defer the actual construction of the multipart::Form to the execute_request function in src/client/req.rs, where it can be properly awaited without blocking the thread. This would also involve changing Multipart::extract to only clone the parts and leave the form field as None initially.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/multipart_test.py (1)

80-90: Consider making discarded Multipart explicit.

Line 87 creates a Multipart whose result is discarded. While this correctly tests the side effect of consuming the stream value, assigning to _ would make the intent clearer.

Suggested change for clarity
     part = Part(name="stream", value=bytes_stream())
-    Multipart(part)
+    _ = Multipart(part)  # Consumes the stream value from part
 
     with pytest.raises(RuntimeError):
         Multipart(part)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/multipart_test.py` around lines 80 - 90, The test creates a Multipart
from part and discards the result implicitly; make that intent explicit by
assigning the created Multipart to a throwaway variable (e.g., _) so it’s clear
the value is intentionally discarded — update the invocation Multipart(part) in
test_reuse_same_part_without_copy_fails_for_stream_value to use an explicit
discard variable while keeping the rest of the test (use the same Part,
bytes_stream, and pytest.raises block).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/multipart_test.py`:
- Around line 80-90: The test creates a Multipart from part and discards the
result implicitly; make that intent explicit by assigning the created Multipart
to a throwaway variable (e.g., _) so it’s clear the value is intentionally
discarded — update the invocation Multipart(part) in
test_reuse_same_part_without_copy_fails_for_stream_value to use an explicit
discard variable while keeping the rest of the test (use the same Part,
bytes_stream, and pytest.raises block).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d8287f23-eaff-48d4-bb7a-857fac92a4c4

📥 Commits

Reviewing files that changed from the base of the PR and between e3ae1a5 and 5ad504b.

📒 Files selected for processing (3)
  • src/client/body/multipart.rs
  • src/client/req.rs
  • tests/multipart_test.py

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/client/body/multipart.rs (1)

141-147: Consider a more descriptive error for exhausted stream values.

Error::Memory may be confusing when the actual cause is that a stream-based part was already consumed. A dedicated error variant like Error::StreamConsumed or Error::ValueExhausted would provide clearer diagnostics to users.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/body/multipart.rs` around lines 141 - 147, The current
clone_value_or_take method in multipart.rs returns Error::Memory when a
stream-based part is already consumed; add a dedicated error variant (e.g.,
Error::StreamConsumed or Error::ValueExhausted) to the Error enum, implement its
conversion to the PyErr/into() path used elsewhere, and replace the
Error::Memory usage in clone_value_or_take (and any other places that
semantically represent an exhausted stream) to return the new variant so callers
get a clear, descriptive error from functions like clone_value_or_take.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/client/body/multipart.rs`:
- Around line 141-147: The current clone_value_or_take method in multipart.rs
returns Error::Memory when a stream-based part is already consumed; add a
dedicated error variant (e.g., Error::StreamConsumed or Error::ValueExhausted)
to the Error enum, implement its conversion to the PyErr/into() path used
elsewhere, and replace the Error::Memory usage in clone_value_or_take (and any
other places that semantically represent an exhausted stream) to return the new
variant so callers get a clear, descriptive error from functions like
clone_value_or_take.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0aa12ccf-112a-4aa2-b61a-46944c26e987

📥 Commits

Reviewing files that changed from the base of the PR and between 5ad504b and 55ff064.

📒 Files selected for processing (3)
  • src/client/body/multipart.rs
  • src/client/req.rs
  • tests/multipart_test.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/multipart_test.py

@0x676e67 0x676e67 merged commit 2efccfc into main Apr 14, 2026
32 checks passed
@0x676e67 0x676e67 deleted the multipart branch April 14, 2026 14:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant