Upgrade ruff to 0.16.3 and ty to 0.0.72 - #99
Conversation
ty 0.0.72 checks descriptor protocol and generic subscripting more strictly than 0.0.65, surfacing real gaps that were previously masked: - ImmutableList subclassed plain `tuple` instead of `tuple[T, ...]`, so it was never actually generic despite being used as ImmutableList[T] everywhere in plain-postgres/meta.py. - SimpleCookie is already BaseCookie[str], not a generic type itself, so `SimpleCookie[str]` was an invalid subscript. - Field/RelatedField/ForeignKeyField's class-body attribute annotations (`field: ForeignKeyField`, etc.) made ty apply descriptor-protocol handling to plain instance attributes, since Field implements __get__. Moved the annotations to __init__ so they're read as normal instance attributes, then fixed the real Field.name: str | None narrowing issues that surfaced once ty could see through them properly (via a new require_field_name() helper for the common "already-contributed field" case, and narrower isinstance checks elsewhere, e.g. select_related_descend now returns TypeIs[ForeignKeyField] instead of a bare bool). - AdminViewset.get_views() stamps get_list_url/get_create_url/etc onto sibling view classes dynamically; declared those as ClassVar-style Callable attributes on AdminView instead of suppressing the assignment, and narrowed the views list to AdminView instead of the bare View base. Also dropped several now-stale `# ty: ignore` comments the new version no longer needs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GCsrEW1kDCAg5BT9suCPZ
Several call sites added by the previous commit hand-rolled the same `assert field.name is not None` narrowing that require_field_name() exists to centralize, instead of calling it. Route them through the helper (and cache to a local `field_name` where it's read more than once), and consolidate a duplicate same-function import. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GCsrEW1kDCAg5BT9suCPZ
`import plainx` in plain/plain/cli/agent.py only resolved locally because this checkout's venv had accumulated plainx-dev from an earlier `uv sync --all-packages`. A clean `uv sync` (as CI's lint job does) never installs it, since it's a workspace member but wasn't listed as a dev dependency — so removing the `# ty: ignore[unresolved-import]` there in the previous commit was premature: `ty check` only passed locally by accident. Confirmed by removing .venv and re-syncing from scratch, which reproduced CI's exact failure. Add plainx-dev as a real dev dependency instead of restoring the ignore, matching the existing openapi-spec-validator precedent for optional runtime deps that ty needs resolvable in every environment. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GCsrEW1kDCAg5BT9suCPZ
There was a problem hiding this comment.
Code review
No issues found. Checked for bugs and CLAUDE.md compliance.
Verified the non-trivial logic rewrites in this ty-upgrade pass against real call sites:
select_related_descendnarrowingRelatedField→ForeignKeyField— behavior-preserving; both call sites already exclude M2M upstream.Model._check_orderingrewrite toisinstance(f, ForeignObjectRel)— equivalent to the oldauto_created and not concreteflag check overchain(meta.fields, meta.related_objects).HttpHeaders._convert_to_charset— underexcept UnicodeError, the three listed classes are its only subclasses carrying.reason, so the explicitisinstancematches the oldhasattrbehavior.ImmutableList.__new__signature tightening — the sole call site passes(data, warning=...), which matches; copy/pickle still round-trip viatuple.__getnewargs__.AdminViewset.get_views()issubclassnarrowed toAdminView— every inner view class across the repo subclassesAdminView, and the newget_*_url = Nonedefaults are shadowed by the real methods inobjects.py/builtin_views.pywith nohasattrguards reading them.require_field_name()threading — all call sites operate on fields reached throughMeta(already contributed), and the new imports introduce no cycles.
One sub-threshold note, not a blocker: select_related_descend is annotated -> TypeIs[ForeignKeyField], but it returns False for plenty of genuine ForeignKeyField inputs (unrequested, nullable), which doesn't satisfy TypeIs's bidirectional contract — TypeGuard is the strictly correct form. Both current call sites continue on the false branch, so there's no runtime or typing impact today.
|
@codex review |
|
Codex Review: Didn't find any major issues. 🚀 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
require_field_name(field) lived in meta.py, which sits in the middle of a real import cycle (fields/base.py -> ... -> meta.py -> query.py -> fields -> fields/base.py), forcing 4 of its 7 call sites to import it locally inside functions instead of at module top. Field is defined in fields/base.py, at the bottom of that import graph, so putting the narrowing on Field itself as a property removes the cycle entirely -- every call site now just reads field.contributed_name, no import needed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GCsrEW1kDCAg5BT9suCPZ
Summary
ruff0.16.2 → 0.16.3 andty0.0.65 → 0.0.72 (both dev dependencies), and re-lock.ty0.0.72 checks descriptor protocol and generic subscripting more strictly than 0.0.65, which surfaced 71 new diagnostics acrossplain-postgres,plain-admin,plain-auth,plain-templates, andplain. All are fixed here — no new ignores were added net of what was removed; several stale# ty: ignorecomments were dropped, and typing was tightened rather than loosened wherever the underlying issue was real:ImmutableListsubclassed plaintupleinstead oftuple[T, ...], so it was never actually generic despite being used asImmutableList[T]throughoutplain-postgres/meta.py. Fixed to a proper PEP 695 generic.SimpleCookieis alreadyBaseCookie[str], not generic itself, soSimpleCookie[str]was an invalid subscript inplain/plain/test/client.py.Field/RelatedField/ForeignKeyField's class-body attribute annotations (e.g.field: ForeignKeyField) madetyapply descriptor-protocol handling to what are actually plain instance attributes, sinceFieldimplements__get__. Moved those annotations into__init__instead, which both matches real Python semantics and lettysee through to genuineField.name: str | Nonenarrowing gaps underneath — fixed via a newField.contributed_nameproperty for the common "already-contributed field" case, and a couple of narrowerisinstance/TypeIschecks (e.g.select_related_descendnow returnsTypeIs[ForeignKeyField]instead of a barebool, matching the realallow_nullavailability difference betweenForeignKeyFieldandManyToManyField).AdminViewset.get_views()dynamically stampsget_list_url/get_create_url/etc. onto sibling view classes; declared those asCallable-typed class attributes onAdminViewinstead of suppressing the assignment, and narrowed the return type from the bareViewbase toAdminView./simplifyafterward; review passes found call sites that hand-rolled the same narrowing the new helper was introduced to centralize — routed those through it, and then relocated the helper itself from a free function inmeta.py(which sits in the middle of an import cycle, forcing several call sites into local imports) ontoFielditself ascontributed_name, sinceFieldsits at the bottom of that import graph — every call site now just reads the property, no import needed.plainx-dev(a workspace member) wasn't listed as a dev dependency, so a cleanuv syncnever installed it —tyonly resolvedimport plainxinplain/cli/agent.pylocally by accident (this checkout's venv had accumulated it from an earlier--all-packagessync). CI'slintjob caught it on a clean checkout; added it to the dev dependency group instead of restoring the ignore.masterin to pick up the concurrent server body-handling rewrite.Test plan
uv run ty check .— clean./scripts/fix/uv run plain-code check .(ruff + ty + annotations) — clean./scripts/type-validate(same check CI'slintjob runs) — 27/27 passed, reproduced from a clean.venv+ Python 3.14./scripts/test(full suite) — plain, plain-postgres, plain-admin, plain-auth, plain-templates, and everything else through plain-loginlink all passedGenerated by Claude Code