Repository navigation
Add a requests-style files argument for multipart uploads - #159
Merged
Merged
Conversation
execute_request, post, put and patch take files= now, in the forms requests accepts: a file object, or (filename, content[, content type[, part headers]]), as a dict or a list of (name, file) pairs to repeat a field. Fields of a dict or list data= go into the same body first. The multipart/form-data body is built with the stdlib under a random boundary and sent as bytes. Names and filenames are quoted like urllib3 does: ", CR and LF are percent-escaped, non-ASCII is sent as UTF-8. File objects in text mode and str content are encoded to UTF-8. Parts with a filename get a Content-Type guessed from it, falling back to application/octet-stream. The multipart Content-Type is set for the one request only, and like the other inferred types it gives way to one the session or request set. files with json= or str/bytes data= raise ValueError. Fixes #35 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A file object, str content and a plain (None, value) field next to data= fields arrive parsed as files and form. bytes(range(256)) comes back intact; httpbin returns it as a data URL carrying the image/png type guessed from the filename. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FlorianREGAZ
force-pushed
the
feat/multipart-files
branch
from
October 10, 2026 11:32
c922085 to
a5aa2a5
Compare
The module sits next to cffi.py, cookies.py and structures.py, none of which use a private prefix. The httpbin test now writes its file in binary, so Windows doesn't turn the newline into CRLF. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
execute_request,post,putandpatchnow acceptfiles=, which sends amultipart/form-databody. It takes the same forms as requests:{"name": fileobj}: the filename is the basename offileobj.name, or the field name if the object has no usable name{"name": ("filename", content)}, where content is a file object, bytes or str{"name": ("filename", content, "content/type")}{"name": ("filename", content, "content/type", {"X-Part-Header": "..."})}(name, file)pairs, to send a field more than once("filename", None)skips the field, and(None, value)sends a plain form fieldWhen
datais a dict or a list of pairs, its fields go into the same body, ahead of the files. List values repeat a field,Nonevalues are skipped and other values are converted withstr().The body is built with the stdlib (
tls_client/_multipart.py), uses a random 32-hex-character boundary, and goes to the library as bytes (isByteRequest, base64). For the parts that requests itself builds, the output is byte-for-byte the same as requests 2.34/urllib3 2.8 with a fixed boundary: names and filenames escape", CR and LF as%22,%0D,%0A, and non-ASCII is sent as UTF-8. Text-mode file objects and str content are encoded to UTF-8.Content-Type: multipart/form-data; boundary=...is set for that one request only, never onsession.headers. A Content-Type that the session or the request set explicitly is kept, as in requests, andNonesends no Content-Type at all.filestogether withjson=, or withdata=given as str or bytes, raisesValueError. Nothing is sent in that case.Fixes #35
Differences from requests
Content-Typeguessed from it withmimetypes, falling back toapplication/octet-stream. requests sends no partContent-Typeunless the tuple gives one.fileswithjson=raisesValueError. requests silently drops the JSON.fileswith str/bytesdata=raises in both.Content-Typein the part headers is used when no content type is given. requests drops it.files={}behaves as if no files were passed, as in requests.Testing
uv run pytest: 363 passed, 7 xfailed (the existing ones)._multipart.pyandsessions.pyhave full line and branch coverage.uv run ruff check,uv run ruff format --check,uv run mypy(strict): clean.tests/unit/test_multipart.pyuse the fake library with the boundary pinned through monkeypatch. They check the exact body bytes and the Content-Type header, filename and field quoting, content-type guessing, mixing data and files, repeated fields, the random boundary, and the error cases./postcheck that the files and form fields come back parsed as expected.bytes(range(256))comes back intact: httpbin returns it as adata:image/png;base64,...URL, using the type guessed from the filename.requests.Request(...).prepare().body, with both boundaries pinned, for data fields, repeated fields, quoting, non-ASCII filenames and extra part headers.🤖 Generated with Claude Code