fix(python): type-check and send all arguments of body-variant overloads

Draft
2Y
6d ago
+707-58

Why

With bodyVariantOverloads, pythonv2 flattens each variant of a oneOf-of-objects request body into keyword arguments. Several calls that type-check were silently wrong at runtime:

  • A trailing **body_kwargs: Any overload accepted any keyword, so misspelled or unknown arguments, and typos inside nested TypedDict arguments, type-checked and were dropped.
  • On operations that also have path, query or header parameters, the flattened call keyed the assembled request by the body schema name instead of the request field name, and parameters passed alongside request= were ignored.
  • A body property whose argument name is sanitized in the method signature (type → type_, or retries → retries_ when it clashes with a per-call option) was dropped from the request body.

What changed

  • Replace the **body_kwargs: Any overload with one **kwargs: Unpack[<Variant>TypedDict] overload per variant, emitted after every expanded overload. A variant gets one only when its expanded overload is narrower than its TypedDict (split stream, discriminator Literal, renamed key) and no TypedDict key collides with a named method parameter. Unpack overloads after the first carry # pyright: ignore[reportOverlappingOverload].
  • Add the keyword-only non-body parameters to the request= overload, and apply path, query and header arguments that are neither UNSET nor None over the values in request.
  • Key the assembled request by each parameter's pydantic alias and the body field name, so flattened calls on operations with parameters, or with x-speakeasy-name-override parameters, validate and send them.
  • Map renamed argument names back to the model field names before unmarshalling the body.
  • Raise ValueError when request and body keyword arguments are both supplied; the body arguments were ignored.
  • Type optional nullable parameters in overloads as OptionalNullable[T], keep nullable property types in expanded overloads, and accept None for optional properties.

Known limitation: a variant whose property names collide with method parameters (retries, timeout_ms, server_url, ...) gets no Unpack overload, so spreading its TypedDict with ** does not type-check. The expanded overload (retries_=...) still works.

Fixture and test IDs

The shared fixture tests/specs/fragments/primary/body-variant-overloads.yaml and the shared test IDs come from #188. This PR:

  • adds a nullable optional query parameter (cursor) to bodyVariantOverloadsDiscriminated, to cover OptionalNullable[T] in overloads;
  • records the shared test IDs in pythonv2 and removes them from its skip list:
    • unions-body-variant-overloads-discriminated
    • unions-body-variant-overloads-non-discriminated
    • unions-body-variant-overloads-request-body
    • unions-body-variant-overloads-non-body-arguments
    • unions-body-variant-overloads-renamed-argument

Testing

  • New tests/primary/test_body_variant_overloads.py:
    • runs pyright over invalid and valid call snippets;
    • checks the overlap suppression placement;
    • over a mock transport, checks the request= plus body arguments ValueError, and that each variant's body, request= vs flattened bodies (including type_), renamed body arguments, and path/query/header arguments reach the HTTP request.
  • TARGET=primary make test-pythonv2: 812 passed, 0 failed, before rebasing onto #188. Not rerun since; the rebase adds the fixture operations from #188 (optional body, parameter-name clash, server URL, open-enum union).
  • make check-template-pythonv2 and make lint pass after the rebase.
  • Not rerun after rebasing onto main: pythonv2 review.

Public-safety check

  • This change contains no credentials, customer documents, private repository URLs, private filesystem paths, or unredacted private logs.
  • Title, body, comments, and commit messages name no customers or customer-derived identifiers, private paths or trackers, or workflow provenance, and are understandable without private context (.claude/skills/public-repo-communication/SKILL.md).
  • Generated fixtures and review SDK changes are public-safe.
  • I reviewed git diff --check.

Summary by cubic

Fixes bodyVariantOverloads calls in the pythonv2 generator that type-checked but were silently wrong at runtime: unknown or misspelled keyword arguments were accepted and dropped, path/query/header arguments passed alongside request= were ignored, flattened body fields were ignored when request= was supplied, and body fields renamed in method signatures (type_, retries_) never reached the request.

  • Replaces the catch-all **body_kwargs: Any overload with per-variant **kwargs: Unpack[<Variant>TypedDict] overloads, so type checkers now reject unknown keys, nested TypedDict typos, and mixed-variant fields.
  • Applies non-body parameter arguments over the request= values, keys the assembled request by pydantic alias and body field name, and maps renamed argument names back to model field names before unmarshalling.
  • Raises ValueError when flattened body keywords are supplied together with request=.
  • Optional arguments accept None and optional nullable parameters are typed as OptionalNullable[T].

Known limitation

Variants whose property names collide with method parameters (retries, timeout_ms, server_url) get no Unpack overload, so spreading their TypedDict with ** does not type-check; the expanded overload (retries_=...) still works.