Repository navigation
Conversation
…s from the current position _invoke_progress tried progress(uploaded, total) and, on TypeError, fell back to progress(chunk_size). The except also caught TypeErrors raised inside the user's callback, so a two-argument callback that failed on its own (the docstring's own example does when total is None: 100 * uploaded / total) was silently called again with one argument and the user saw 'missing 1 required positional argument: total' instead of their error. Arity now comes from inspect.signature once per call; callables that cannot be inspected get the documented two-argument form. _chunk_file_from_fileobj (sync and async) computed total_bytes as the absolute end offset, so a file object positioned past a header reported a total the upload could never reach. It is now end minus current position, which is what gets uploaded. Tests: one-argument plain function gets chunk sizes; a TypeError from inside the callback propagates with its own message; a BytesIO read past a header reports (payload, payload) on the last call. Both new behaviours fail on main.
AsyncClient.files.upload accepted an async def on_progress callback, called it from the async chunk generators through the sync _invoke_progress, and dropped the returned coroutine. The callback body never ran and Python emitted 'coroutine ... was never awaited' once per chunk. Writing the callback as a coroutine is the natural thing to do in async code and the type did not forbid it. _invoke_progress now returns the callback's result; the three async generators call _async_invoke_progress, which awaits the result when it is awaitable. Sync callbacks and tqdm-like objects are unchanged. ProgressCallback admits awaitable-returning callables. Test: an async def callback on AsyncClient.files.upload runs on every chunk with RuntimeWarning raised as an error; fails on main.
deanrie
force-pushed
the
files-async-progress
branch
from
October 24, 2026 09:54
7e838d8 to
4f52640
Compare
4 tasks done
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.
Stacked on #232 (same file, same helper); the diff here is the last commit. Merge #232 first, or merge this and #232 comes with it.
Checklist
Description
AsyncClient.files.uploadaccepts anon_progresscallback and calls it from the three async chunk generators through the sync_invoke_progress. When the callback is anasync def, the natural shape in async code and one theProgressCallbacktype did not forbid, the call returns a coroutine that nobody awaits: the callback body never runs, and Python warnscoroutine '...' was never awaitedonce per chunk.Change
_invoke_progressreturns whatever the callback returned._async_invoke_progress, which awaits that value when it is awaitable. Sync callbacks and tqdm-like objects take the same path as before (their return value isNone).ProgressCallbackadmits callables returningAwaitable[None], so anasync deftype-checks.The sync client is unchanged: it cannot await, and an
async defpassed there still produces the same warning it always did; that remains a caller error on the sync path.Tests
tests/aio/files_test.py: anasync defcallback onAsyncClient.files.uploadis called for both chunks with(uploaded, total)and ends at(len(data), len(data)), withRuntimeWarningturned into an error for the duration. Fails onmainwith the never-awaited warning.uv run pytest tests/aio/files_test.py tests/sync/files_test.py: 75 passed.ruff check,ruff format --check,pyrightclean on the touched files.