Skip to content

Upgrade ruff to 0.16.4 and ty to 0.0.74 - #103

Draft
davegaeddert wants to merge 2 commits into
masterfrom
claude/peaceful-meitner-l8cko5
Draft

Upgrade ruff to 0.16.4 and ty to 0.0.74#103
davegaeddert wants to merge 2 commits into
masterfrom
claude/peaceful-meitner-l8cko5

Conversation

@davegaeddert

@davegaeddert davegaeddert commented Aug 24, 2026

Copy link
Copy Markdown
Member

Supersedes #99, which upgraded to ruff 0.16.3 / ty 0.0.72 a week ago and has since fallen behind master. Both PRs independently diagnosed the same three root causes; this one goes to the newer versions, sits on current master, and takes a different (smaller) approach to the main one — see below.

Summary

  • Bumps ruff 0.16.2 → 0.16.4 (within the existing <0.17 bound) and ty 0.0.65 → 0.0.74.
  • ty's stricter checking surfaced a real, widespread latent issue: Field.name was typed str | None (only None before contribute_to_class), which had been masked by ImmutableList — used throughout plain-postgres/meta.py for field collections — not actually being generic. Fixing ImmutableList to a proper class ImmutableList[T](tuple[T, ...]) unmasked ~30 sites relying on field.name always being a string.
  • Rather than patching each site, retyped Field.name as str (default "") at the source. A sub-agent traced every read, write, and deconstruct() consumer of the None first: nothing branches on it, the one behavioral difference was a __repr__ is not None check (now a truthiness check), and deconstruct()'s name is discarded by every one of its ~15 consumers. This removes the scattered asserts rather than adding an accessor to work around them.
  • Genuine descriptor-protocol false positives in plain-postgres's related-field classes: Field implements __get__, so a class-body field: ForeignKeyField annotation made ty resolve every self.field read through the descriptor protocol. Moved those annotations to the assignment site in __init__, which matches real Python semantics.
  • SimpleCookie[str] is an invalid subscript — SimpleCookie is already BaseCookie[str], not itself generic.
  • UnicodeError.reason narrowed via isinstance against the three concrete stdlib subclasses that declare it, replacing a hasattr duck-check that typed it as Any.
  • Widened a few list[Field] parameters that only read/iterate their input to Sequence[Field], so an ImmutableList passes without copying.
  • Hoisted plainx-dev into the dev dependency group. It's a workspace member, but import plainx in plain/cli/agent.py carried a # ty: ignore[unresolved-import] because a clean uv sync — what CI's lint job runs — never installed it; it only resolved in checkouts whose venv had accumulated it from an earlier --all-packages sync. Credit to Upgrade ruff to 0.16.3 and ty to 0.0.72 #99, which found this independently; verified here from a deleted .venv, the only way to reproduce CI's resolution.
  • Ran /simplify on the resulting diff (4 parallel review agents: reuse, simplification, efficiency, altitude) and applied what it found — reverted a redundant cached attribute, relocated get_joining_columns from ForeignObjectRel down to ForeignKeyRel where it's actually reachable (deleting a NotImplementedError stub and narrowing two join_field annotations to match), swapped a semantically-wrong TypeIs for TypeGuard, and replaced a dict.update() stdlib bypass with an approach that preserves Morsel's own reserved-key validation.

Net effect on the repo's own type-health counters (./scripts/type-validate), measured against master's source on the new ty:

master this PR
paths passing 25/26 26/26
type ignores 155 148
cast() calls 46 45
assert statements 196 173

Test plan

  • uv run plain-code check . (ruff + ty + annotations) clean — oxlint/oxfmt skipped, no JS/CSS touched and their binary download is blocked by this sandbox's network policy
  • Both CI lint steps reproduced from a deleted .venv: ./scripts/type-validate 26/26, ./scripts/test-package-imports all clean
  • Full test suite (./scripts/test --server) passes across all packages, plus server conformance/load/resilience and MCP conformance
  • One unrelated flaky test (plain-portal's birthday-paradox random-code-collision assertion) reproduced and confirmed pre-existing by rerunning in isolation

claude added 2 commits August 24, 2026 16:08
ty's stricter checking surfaced a wide class of latent str | None
issues around Field.name (only None before contribute_to_class), plus
a genuinely non-generic ImmutableList(tuple) subclass and a few
descriptor-protocol false positives. Retyping Field.name as str
(default "") and making ImmutableList generic removes ~30 scattered
`assert x.name is not None` guards instead of adding new ones.

Other fixes: SimpleCookie is not itself generic in typeshed (it's
already BaseCookie[str]), UnicodeError.reason needs narrowing to its
three concrete subclasses, and a couple of list[...] parameters were
widened to Sequence[...] where ImmutableList was being passed in.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015HY8ykWPUqsiwvfAJ38oLM
`import plainx` in plain/cli/agent.py carried a
`# ty: ignore[unresolved-import]` because plainx-dev, though a workspace
member, was never listed as a dev dependency. A clean `uv sync` — which
is what CI's lint job runs — never installs it, so the import only
resolved in checkouts whose venv had accumulated it from an earlier
`uv sync --all-packages`.

Add it as a real dev dependency instead of keeping the ignore, matching
the existing openapi-spec-validator precedent for deps that ty needs
resolvable in every environment. Verified from a deleted .venv, which is
the only way to reproduce CI's resolution.

Credit to #99, which found this independently.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015HY8ykWPUqsiwvfAJ38oLM
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