Skip to content

Upgrade ruff to 0.16.3 and ty to 0.0.72 - #99

Draft
davegaeddert wants to merge 5 commits into
masterfrom
claude/peaceful-meitner-mvqq0y
Draft

Upgrade ruff to 0.16.3 and ty to 0.0.72#99
davegaeddert wants to merge 5 commits into
masterfrom
claude/peaceful-meitner-mvqq0y

Conversation

@davegaeddert

@davegaeddert davegaeddert commented Aug 17, 2026

Copy link
Copy Markdown
Member

Summary

  • Bump ruff 0.16.2 → 0.16.3 and ty 0.0.65 → 0.0.72 (both dev dependencies), and re-lock.
  • ty 0.0.72 checks descriptor protocol and generic subscripting more strictly than 0.0.65, which surfaced 71 new diagnostics across plain-postgres, plain-admin, plain-auth, plain-templates, and plain. All are fixed here — no new ignores were added net of what was removed; several stale # ty: ignore comments were dropped, and typing was tightened rather than loosened wherever the underlying issue was real:
    • ImmutableList subclassed plain tuple instead of tuple[T, ...], so it was never actually generic despite being used as ImmutableList[T] throughout plain-postgres/meta.py. Fixed to a proper PEP 695 generic.
    • SimpleCookie is already BaseCookie[str], not generic itself, so SimpleCookie[str] was an invalid subscript in plain/plain/test/client.py.
    • Field/RelatedField/ForeignKeyField's class-body attribute annotations (e.g. field: ForeignKeyField) made ty apply descriptor-protocol handling to what are actually plain instance attributes, since Field implements __get__. Moved those annotations into __init__ instead, which both matches real Python semantics and let ty see through to genuine Field.name: str | None narrowing gaps underneath — fixed via a new Field.contributed_name property for the common "already-contributed field" case, and a couple of narrower isinstance/TypeIs checks (e.g. select_related_descend now returns TypeIs[ForeignKeyField] instead of a bare bool, matching the real allow_null availability difference between ForeignKeyField and ManyToManyField).
    • AdminViewset.get_views() dynamically stamps get_list_url/get_create_url/etc. onto sibling view classes; declared those as Callable-typed class attributes on AdminView instead of suppressing the assignment, and narrowed the return type from the bare View base to AdminView.
  • Ran /simplify afterward; 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 in meta.py (which sits in the middle of an import cycle, forcing several call sites into local imports) onto Field itself as contributed_name, since Field sits 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 clean uv sync never installed it — ty only resolved import plainx in plain/cli/agent.py locally by accident (this checkout's venv had accumulated it from an earlier --all-packages sync). CI's lint job caught it on a clean checkout; added it to the dev dependency group instead of restoring the ignore.
  • Merged master in 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's lint job 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 passed

Generated by Claude Code

claude added 2 commits August 17, 2026 14:43
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
@davegaeddert
davegaeddert marked this pull request as ready for review August 18, 2026 20:17
claude added 2 commits August 18, 2026 20:23
`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
@davegaeddert
davegaeddert marked this pull request as draft August 18, 2026 20:33

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_descend narrowing RelatedFieldForeignKeyField — behavior-preserving; both call sites already exclude M2M upstream.
  • Model._check_ordering rewrite to isinstance(f, ForeignObjectRel) — equivalent to the old auto_created and not concrete flag check over chain(meta.fields, meta.related_objects).
  • HttpHeaders._convert_to_charset — under except UnicodeError, the three listed classes are its only subclasses carrying .reason, so the explicit isinstance matches the old hasattr behavior.
  • ImmutableList.__new__ signature tightening — the sole call site passes (data, warning=...), which matches; copy/pickle still round-trip via tuple.__getnewargs__.
  • AdminViewset.get_views() issubclass narrowed to AdminView — every inner view class across the repo subclasses AdminView, and the new get_*_url = None defaults are shadowed by the real methods in objects.py/builtin_views.py with no hasattr guards reading them.
  • require_field_name() threading — all call sites operate on fields reached through Meta (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.

@davegaeddert

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

Reviewed commit: 7a18077a21

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants