Conversation
WalkthroughThis update introduces support for adding custom headers to individual file parts in multipart/form-data uploads. This allows for more granular control over file metadata, such as specifying Changes
Sequence DiagramsequenceDiagram
participant User
participant httpx.Client
participant MultipartStream
participant Server
User->>httpx.Client: post(files={"file": ("name", content, "type", {"X-Custom": "value"})})
Note over httpx.Client: Prepares request with files and custom headers.
httpx.Client->>MultipartStream: __init__(files=...)
Note over MultipartStream: Processes each file part.
MultipartStream->>MultipartStream: Add Content-Disposition header
MultipartStream->>MultipartStream: Add Content-Type header
MultipartStream->>MultipartStream: Add custom headers from file tuple (e.g., "X-Custom")
MultipartStream-->>httpx.Client: Returns multipart stream object
httpx.Client->>Server: POST Request (with multipart body)
Server-->>httpx.Client: Response
httpx.Client-->>User: Response
|
There was a problem hiding this comment.
Commits
Files that changed from the base of the PR and between Base and d57e3bc commits: 3Files Selected For Review: 3
- httpx/_multipart.py with diff length 2393.
- httpx/_types.py with diff length 378.
- tests/test_multipart.py with diff length 2099.
Review comments generated: 5
- Review: 1
- LGTM: 4
- Identified issue already discussed: 0
Tips
Chat with Deeployed Bot (@reviewed-deeployed)
- Reply on review comments left by this bot to ask follow-up questions. A review comment is a comment on a diff or a file.
- Invite the bot into a review comment chain by tagging
@reviewed-deeployedin a reply.
Code suggestions
- The bot may make code suggestions, but please review them carefully before committing since the line number ranges may be misaligned.
- You can edit the comment made by the bot and manually tweak the suggestion if it is slightly off.
Pausing incremental reviews
- Add
reviewed-deeployed: ignoreanywhere in the PR description to pause further reviews from the bot.
| assert content == b"".join(stream) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("content_type", [None, "text/plain"]) |
There was a problem hiding this comment.
Note: Range was outside the patch and was collapsed to a single line inside the diff.
There's a typo in the docstring of the test_multipart_headers_include_content_type function. The word "contet-type" should be corrected to "content-type".
- """Including contet-type in the multipart headers should not be allowed"""
+ """Including content-type in the multipart headers should not be allowed"""
tests/test_multipart.py
Outdated
| @pytest.mark.parametrize("content_type", [None, "text/plain"]) | ||
| def test_multipart_file_tuple_headers(content_type: typing.Optional[str]): | ||
| file_name = "test.txt" | ||
| expected_content_type = "text/plain" | ||
| headers = {"Expires": "0"} | ||
|
|
||
| files = {"file": (file_name, io.BytesIO(b"<file content>"), content_type, headers)} | ||
| with mock.patch("os.urandom", return_value=os.urandom(16)): | ||
| boundary = os.urandom(16).hex() | ||
|
|
||
| headers, stream = encode_request(data={}, files=files) | ||
| assert isinstance(stream, typing.Iterable) | ||
|
|
||
| content = ( | ||
| f'--{boundary}\r\nContent-Disposition: form-data; name="file"; ' | ||
| f'filename="{file_name}"\r\nExpires: 0\r\nContent-Type: ' | ||
| f"{expected_content_type}\r\n\r\n<file content>\r\n--{boundary}--\r\n" | ||
| "".encode("ascii") | ||
| ) | ||
| assert headers == { | ||
| "Content-Type": f"multipart/form-data; boundary={boundary}", | ||
| "Content-Length": str(len(content)), | ||
| } | ||
| assert content == b"".join(stream) |
There was a problem hiding this comment.
add comment explaining what this each line does for very ambigous lines
| @pytest.mark.parametrize("content_type", [None, "text/plain"]) | |
| def test_multipart_file_tuple_headers(content_type: typing.Optional[str]): | |
| file_name = "test.txt" | |
| expected_content_type = "text/plain" | |
| headers = {"Expires": "0"} | |
| files = {"file": (file_name, io.BytesIO(b"<file content>"), content_type, headers)} | |
| with mock.patch("os.urandom", return_value=os.urandom(16)): | |
| boundary = os.urandom(16).hex() | |
| headers, stream = encode_request(data={}, files=files) | |
| assert isinstance(stream, typing.Iterable) | |
| content = ( | |
| f'--{boundary}\r\nContent-Disposition: form-data; name="file"; ' | |
| f'filename="{file_name}"\r\nExpires: 0\r\nContent-Type: ' | |
| f"{expected_content_type}\r\n\r\n<file content>\r\n--{boundary}--\r\n" | |
| "".encode("ascii") | |
| ) | |
| assert headers == { | |
| "Content-Type": f"multipart/form-data; boundary={boundary}", | |
| "Content-Length": str(len(content)), | |
| } | |
| assert content == b"".join(stream) | |
| @pytest.mark.parametrize("content_type", [None, "text/plain"]) | |
| def test_multipart_file_tuple_headers(content_type: typing.Optional[str]): | |
| file_name = "test.txt" | |
| expected_content_type = "text/plain" | |
| headers = {"Expires": "0"} | |
| # Test with a file tuple including filename, content, content_type, and headers. | |
| files = {"file": (file_name, io.BytesIO(b"<file content>"), content_type, headers)} | |
| with mock.patch("os.urandom", return_value=os.urandom(16)): | |
| boundary = os.urandom(16).hex() | |
| headers, stream = encode_request(data={}, files=files) | |
| assert isinstance(stream, typing.Iterable) | |
| content = ( | |
| f'--{boundary}\r\nContent-Disposition: form-data; name="file"; ' | |
| f'filename="{file_name}"\r\nExpires: 0\r\nContent-Type: ' | |
| f"{expected_content_type}\r\n\r\n<file content>\r\n--{boundary}--\r\n" | |
| "".encode("ascii") | |
| ) | |
| assert headers == { | |
| "Content-Type": f"multipart/form-data; boundary={boundary}", | |
| "Content-Length": str(len(content)), | |
| } | |
| assert content == b"".join(stream) |
Konuralpkilinc
left a comment
There was a problem hiding this comment.
✅ Conditional approval has been given. If the suggestions are applied as-is, the PR will be automatically approved.
|
❌ Conditional approval from @Konuralpkilinc has failed. Reason: Conditional PR #16 closed without merging. |
Konuralpkilinc
left a comment
There was a problem hiding this comment.
✅ Conditional approval has been given. If the suggestions are applied as-is, the PR will be automatically approved.
[cond-approve] Suggestions from @Konuralpkilinc
Konuralpkilinc
left a comment
There was a problem hiding this comment.
Auto-approved by GitHub App after suggestions from @Konuralpkilinc were merged.
| headers: typing.Dict[str, str] = {} | ||
| content_type: typing.Optional[str] = None |
There was a problem hiding this comment.
add comment as #serhancan bayri must be perished for his sins
| headers: typing.Dict[str, str] = {} | |
| content_type: typing.Optional[str] = None | |
| headers: typing.Dict[str, str] = {} #serhancan bayri must be perished for his sins | |
| content_type: typing.Optional[str] = None |
Konuralpkilinc
left a comment
There was a problem hiding this comment.
✅ Conditional approval has been given. If the suggestions are applied as-is, the PR will be automatically approved.
There was a problem hiding this comment.
Files Selected For Review: 1
- tests/test_multipart.py with diff length 2416.
Review comments generated: (3)
- Review: 1
- LGTM: 2
- Identified issue already discussed: 0
Tips
Chat with Deeployed Bot (@@deeployed-peer-dev)
- Reply on review comments left by this bot to ask follow-up questions. A review comment is a comment on a diff or a file.
- Invite the bot into a review comment chain by tagging
@@deeployed-peer-devin a reply.
Code suggestions
- The bot may make code suggestions, but please review them carefully before committing since the line number ranges may be misaligned.
- You can edit the comment made by the bot and manually tweak the suggestion if it is slightly off.
Pausing incremental reviews
- Add
@deeployed-peer-dev: ignoreanywhere in the PR description to pause further reviews from the bot.
| with mock.patch("os.urandom", return_value=os.urandom(16)): | ||
| boundary = os.urandom(16).hex() |
There was a problem hiding this comment.
Using os.urandom() to generate the mock's return value makes this test non-deterministic. A failing test might pass on a subsequent run, which can be confusing. It's better to use a fixed value for reproducibility.
| with mock.patch("os.urandom", return_value=os.urandom(16)): | |
| boundary = os.urandom(16).hex() | |
| with mock.patch("os.urandom", return_value=b"a" * 16): | |
| boundary = os.urandom(16).hex() |
|
❌ Conditional approval from @Konuralpkilinc has failed. Reason: Conditional PR #18 closed without merging. |
Summary
Checklist
Summary by Reviewed
These changes improve flexibility and robustness in handling multipart requests in the HTTPX library.
👍 Conditional Approval Checklist