fix(client): restore pyright field-access checks on lazy models#629
fix(client): restore pyright field-access checks on lazy models#629RapidPoseidon wants to merge 2 commits into
Conversation
The job endpoint response model (GetJobByIdEndpointOutput) was regenerated in 3.14.2 with its `status` field renamed to `state` (now an AudienceJobState enum), but RapidataJob.get_status() still read `.status`, raising AttributeError on every job status check (e.g. display_progress_bar, get_results). Read `.state.value` to restore the str return contract; the enum values (Completed, Failed, ...) already match the strings the callers compare against. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-Authored-By: lino <lino@rapidata.ai>
LazyValidatedModel overrode __getattribute__ with a `-> Any` return, which is class-visible to type checkers. pyright then treats every attribute access on any generated model as a valid `Any`, so renamed or removed backend fields (e.g. reading `.status` after it became `.state`) pass the type-check CI and only blow up at runtime. Guard the override behind `if not TYPE_CHECKING:` — the same trick Pydantic uses for its own __getattr__/__setattr__ (see pydantic/main.py and pydantic#8643). Runtime behaviour is unchanged (TYPE_CHECKING is False at runtime, so the lazy access-time guard still runs); pyright now resolves attribute access against each model's declared field annotations and flags unknown fields. Verified: pyright flags `.status` on GetJobByIdEndpointOutput while `.state` stays clean, and lazy construct/access still works at runtime. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-Authored-By: lino <lino@rapidata.ai>
Code ReviewOverviewThis PR fixes a type-checking blind spot that allowed pyright to silently accept renamed/removed backend fields (the immediate cause:
|
| Area | Status |
|---|---|
| Core fix correctness | ✅ |
| Runtime behaviour unchanged | ✅ |
| Downstream field-access fix | ✅ |
| Mustache templates | ✅ No update needed |
| Pyright 0 errors | ✅ (per PR description) |
| Test coverage for runtime guard |
The fix is correct, minimal, and well-motivated. The only actionable suggestion is adding a regression test for the runtime __getattribute__ guard.
Why
The job-status break fixed in #628 (
.status→.staterename) sailed through thetype-checkCI and only surfaced at runtime in the nightlyrapidata-testingjob. This PR fixes the reason pyright couldn't catch it.LazyValidatedModel(the base class of every generated API model) overrides__getattribute__with a-> Anyreturn. That override is class-visible to type checkers, so pyright treats every attribute access on any generated model as a validAny— renamed/removed backend fields are silently accepted at type-check time and blow up only at runtime.Stock Pydantic avoids exactly this by hiding its own
__getattr__/__setattr__behindif not TYPE_CHECKING:(seepydantic/main.pyand pydantic#8643). The lazy base re-opened the hole.Fix
Guard the
__getattribute__override behindif not TYPE_CHECKING:. One file, in the generated-client base class — so it protects all models at once.TYPE_CHECKINGisFalseat runtime, so the lazy access-time guard still runs.Verification
pyright src/rapidata/rapidata_client-> 0 errors (no fallout; the.statusaccess was fixed in fix(job): read job state field renamed from status #628).model_construct+ reading.stateworks; reading a removed field still raises as before.This would have failed the
type-checkjob on the #624 regen PR instead of the downstream nightly run.🔗 Session: https://session-0eb86da9.poseidon.rapidata.internal/