Type check the whole project instead of seam/resources with error codes disabled - #608
Merged
Merged
Conversation
seam/__init__.py carried a blanket "# type: ignore", which hid the module
from type checkers entirely. Since the package also ships py.typed, every
downstream project that type checks its own code saw the documented import
fail:
app.py:1: error: Module "seam" has no attribute "Seam" [attr-defined]
The lint step only pointed mypy at seam/resources, so nothing in this
repository could catch it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8duZRXUbcxu7y5WeoACqM
SeamHttpClient overrides request to return the decoded body rather than the Response that niquests.Session promises, but the verb helpers kept the inherited Response return type. Indexing what post and get actually return did not type check, which accounted for every "Value of type Response is not indexable" error in the generated routes. Both helpers now name the arguments they forward: Session.request takes params in the position Session.post gives data, so collecting them into *args would have rerouted positional calls. Handling of a response without a status code is no longer a TypeError. niquests types status_code as optional because a Response exists before it has one, so the comparison against it is guarded and the narrowed value is passed to the error path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8duZRXUbcxu7y5WeoACqM
Type checking these files for the first time turned up a latent crash: SeamPaginator.flatten_to_list read pagination.has_next_page without the None guard its sibling flatten already had, so a page whose response carried no pagination raised AttributeError instead of ending the loop. The rest are annotations that did not match their callers. request_id is read from a response header that may be absent, so the HTTP exceptions take Optional[str]. SeamActionAttemptTimeoutError documented its timeout as str while every caller passes a float. poll_until_ready declared its timeout and polling interval Optional though neither is ever None, which made arithmetic on them unsound. SeamPaginator.params used an implicit Optional default, and its response hook is handed either side of the exchange, so the request side is turned away before the pagination is read. A failed action attempt is assumed to carry an error; reading through it unguarded would raise AttributeError over the actual failure. Two invariants are beyond what the checker can follow and are ignored in place: the auth option guards return True only once the values they check are set, and Seam borrows Routes.__init__ to attach the route namespaces even though the two are siblings under AbstractRoutes rather than parent and child. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8duZRXUbcxu7y5WeoACqM
Generated dataclasses declared every field as present and non-null, but from_dict reads each one with dict.get, so a field the payload omits arrives as None behind a type that rules None out. That mismatch was the whole of the ~1720 arg-type errors the lint step disabled the error code to hide. Fields the blueprint marks optional or nullable are now generated as Optional. So are the ones a merged shape cannot guarantee: a property only some variants of a discriminated union carry is absent whenever the dataclass holds a variant that omits it. Nested objects are Optional regardless of what the schema says, because the schema is not a reliable guide to when they arrive. An action attempt documents both error and result as required, yet a pending one carries neither, so from_dict keeps its None fallback and the field admits it. No field gains a default, so the constructors are unchanged. from_dict now takes Any. The payload is decoded JSON and every value read out of it is untyped, so keeping that at the boundary avoids casting each read while the fields carry the real types. Route methods annotate json_payload as Dict[str, Any]. Left bare, its type was inferred from whichever parameter was written first, and every later parameter of a different type was reported as an incompatible assignment. BREAKING CHANGE: Resource fields that the API may omit or send as null are now typed Optional. Code that reads them under a type checker may need a None check. Runtime behavior is unchanged: those fields could already be None. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8duZRXUbcxu7y5WeoACqM
The mypy step checked seam/resources with two error codes disabled. That covered 34 of the 94 files in the package and left every hand-written module unchecked, including the package root whose blanket ignore broke type checking for every downstream user. It now checks seam and test with nothing disabled. The import-not-found disable is dropped outright: it suppressed nothing, since pointing mypy at the whole package resolves the imports it was added for. The paginator tests narrow the pagination they assert on, the way test_paginator_first_page already did, and the test that passes None as a cursor on purpose says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8duZRXUbcxu7y5WeoACqM
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.
Why
The mypy step checked
seam/resourceswith two error codes disabled:That covered 34 of the 94 files in the package and left every hand-written module unchecked. One of them was broken in a way that affected every downstream user:
seam/__init__.pycarried a blanket# type: ignore, so despite the package shippingpy.typed, the documented import failed for anyone type checking their own code.Nothing in this repository could catch that, because the file was never checked.
What changed
just lintnow runsuv run mypy seam testwith nothing disabled. Getting there meant fixing what the flags were hiding — 2150 errors across 76 files, from four root causes:from_dictreads every field withdict.get, so fields the payload omits arrive asNonebehind types that ruleNoneoutOptionalfield types +from_dicttakes the decoded payload asAnyjson_payload = {}had its type inferred from whichever parameter was written firstDict[str, Any]SeamHttpClientoverridesrequestto return the decoded body, butpost/getkept the inheritedResponsereturn type--disable-error-code=import-not-foundis dropped outright. It suppressed nothing — it was only needed because mypy was pointed at a subtree; checking the package resolves those imports.Bugs this surfaced
seam/__init__.pyhid the package from type checkers, breakingfrom seam import Seamfor downstream users (above).SeamPaginator.flatten_to_listreadpagination.has_next_pagewithout theNoneguard its siblingflattenalready had, so a page whose response carried no pagination raisedAttributeErrorinstead of ending the loop.SeamHttpClient._handle_responsecomparedresponse.status_codewithout accounting for it being unset, which would raiseTypeErrorrather than handling the response.SeamActionAttemptFailedErrorreadaction_attempt.error.messageunguarded, which would raiseAttributeErrorover the actual failure.SeamActionAttemptTimeoutErrordocumented itstimeoutasstrwhile every caller passes afloat.request_idasstr, though it comes from a response header that may be absent.Note on the generated
OptionaltypesThis is the breaking part, which is why it is aimed at
beta.Resource fields that the API may omit or send as null are now typed
Optional. Runtime behavior is unchanged — those fields could already beNone, the annotations just denied it — but code that reads them under a type checker may now need aNonecheck.Nested objects are
Optionalregardless of what the schema says. The schema is not a reliable guide here: an action attempt documents botherrorandresultas required, yet a pending one carries neither. Constructing them unconditionally would fail on those payloads, sofrom_dictkeeps itsNonefallback and the field admits it.No field gains a default, so the dataclass constructors are unchanged.
Two invariants are beyond what the checker can follow and are ignored in place with a comment: the auth option guards return
Trueonly once the values they check are set, andSeamborrowsRoutes.__init__to attach the route namespaces even though the two are siblings underAbstractRoutesrather than parent and child.Verification
mypy seam test— clean, 109 files, no disabled error codespylint ./seam ./test— 10.00/10black --check .,rstcheck README.rst— cleanpytest --cov=./seam— 83 passedtsc,eslint,prettier --check— clean for the codegen changesnpm run generate, not edited by handGenerated by Claude Code