Repository navigation
refactor: declare per-request options once as TypedDicts in client.py - #186
Merged
Merged
Conversation
Every verb, *_with_response sibling and stream() in both clients now takes **kwargs: Unpack[_RequestOptions] or Unpack[_BodyRequestOptions]. The pass-through layers _request_with_body, _request_with_body_with_response and _assemble_request_kwargs are gone; _request_kwargs drops options left at their default and raises TypeError on an unknown keyword.
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.
What changed
client.pyspelled out the same per-request keywords (params,headers,cookies,timeout,extensions, plusjson,content,data,filesfor methods that can send a body) on everyoverload, every implementation and every private layer they were forwarded through:
verb →
_request_with_body/_request_with_body_with_response→_prepare_request→_assemble_request_kwargs. Both clients carried a copy.They are now declared once:
get,head,optionsandget_with_responsetake**kwargs: Unpack[_RequestOptions].post,put,patch,delete,request, their*_with_responsesiblings andstream()take**kwargs: Unpack[_BodyRequestOptions]. The base is open and the two leaves are closed because aclosed TypedDict cannot be extended, and the type checker only rejects unknown keywords when the
TypedDict is closed.
_request_with_body,_request_with_body_with_responseand_assemble_request_kwargsare gone.Each verb now calls
_prepare_requestand then_sendor_send_with_response.stream()sharesthe body options and goes through the same
_request_kwargshelper.send(),send_with_response()andbuild_request()are untouched. All the# noqa: PLR0913comments inclient.pyare gone; none is needed any more.Before and after:
AsyncClient.getBefore (the two overloads repeat the same five keywords, with
response_model: None = Noneandresponse_model: type[T]):_request_with_bodythen forwarded all nine keywords to_prepare_request, which forwarded themto
_assemble_request_kwargs.After:
(ruff format puts one parameter per line in the file.)
Line counts
src/httpware/client.py: 2,314 → 1,436 lines (1,096 deleted, 218 added, net −878). Across thewhole PR, tests included: 1,200 deleted, 375 added.
Caller-facing API
Unchanged. Method names are the same,
url(andmethodforrequest/stream) is stillpositional, every option is still keyword-only, and the keyword names are the same per method.
"Not given" means the same thing as before:
None, is not passed to httpx2;timeoutis the exception: absent orhttpx2.USE_CLIENT_DEFAULTmeans the client default, andan explicit
timeout=Nonemeans no timeout (the 0.22.0 rule)._request_kwargsencodes this with one map per TypedDict from option name to its "not given"value (
None, orUSE_CLIENT_DEFAULTfortimeout) and drops a value thatisthat sentinel. Theold tests for
_assemble_request_kwargswere ported to it.Hover trade-off: an IDE hover on
client.getnow shows**kwargs: Unpack[_RequestOptions]insteadof the five named parameters, though completion and type checking still list and check each
keyword (there is no generated API reference to worry about, ADR-0007).
Unknown keywords
With a type checker,
tytreats**kwargs: Unpack[<closed TypedDict>]as exactly those keywords, soclient.get(url, foo=1)andclient.get(url, json=...)fail withno-matching-overload, andclient.post_with_response(url, response_model=M, foo=1)fails withunknown-argument.tests/test_client_typing.pynow has those calls (plusrequest,request_with_responseandstream, sync and async), each with a# ty: ignore[<code>]. A new ty override inpyproject.tomlmakesunused-ignore-commentan error for that file only, so if a TypedDict everstops rejecting unknown keywords, the ignore becomes unused and
just lintfails. I checked this byremoving
closed=Truefrom_RequestOptions: ty reported four unused-ignore errors. The file alsogained
assert_typechecks thatget(..., params=..., timeout=None)still returnsResponse,post(..., json=..., response_model=M)returnsM, andrequest_with_responsereturnstuple[Response, M].At runtime, Python no longer rejects the keyword while binding arguments, so
_request_kwargsdoesit. It already walks the given options to drop the "not given" ones, and the same dict lookup that
finds an option's sentinel also finds an unknown name, so the check adds no work per request. The
error mirrors Python's own:
Tests cover every verb and
*_with_responsemethod in both clients, plusrequest,request_with_responseandstream.One small difference:
stream()is a context manager, so the TypeError now comes from entering it(
with client.stream(..., foo=1):) rather than from the call expression. In normal use both arethe same line.
Parity test
_parameter_shapeintests/test_client_parity.pyexpands a**kwargs: Unpack[TD]parameter intothe keyword-only names of
TD. Sync/async drift in the options (or one world pointing at aTypedDict with different keys) is still caught, and the comparison lists each keyword a caller can
pass. The constructor check gains the same expansion for
_ClientOptionsand_AsyncClientOptions. The close/aclose and AsyncTimeout exceptions are unchanged.Performance
In-process A/B: the origin/main package was imported under another name next to this branch and
the two were timed alternately, best of 3 per round, 16 rounds, then the paired new/base ratio per
round. The machine was heavily loaded (load average 13 to 55), so absolute numbers moved a lot
between runs; the ratios were stable.
"Forwarding" stubs out
httpx2.Client.build_requestand_send, which isolates the code this PRtouches. "Full" is a real call on
httpx2.MockTransport.get, forwardingget, forwardingpost(json=...), forwardingpost(json=...), forwardingget(params=..., timeout=...), forwardingThe first cut checked for streaming bodies with three
dict.getcalls on every request, which madeget(params=..., timeout=...)about 5% slower than main (ratio 1.05). It now tests whethercontent,dataorfilesis present before looking any of them up.Behaviour change for the 0.22.0 notes
None for callers. The one visible difference is where an unknown keyword to
stream()raises (onentering the context manager instead of at the call). The message text is unchanged for
Clientand
AsyncClientinstances; a subclass instance now gets its own class name in the message wherePython used to print the defining class.