Skip to content

feat(api): migrate Tag(Key|Value)Serializer to Serializer[T]#116743

Merged
azulus merged 2 commits into
masterfrom
jeremy/migrate-tag-serializers-to-generic
Jun 3, 2026
Merged

feat(api): migrate Tag(Key|Value)Serializer to Serializer[T]#116743
azulus merged 2 commits into
masterfrom
jeremy/migrate-tag-serializers-to-generic

Conversation

@azulus
Copy link
Copy Markdown
Member

@azulus azulus commented Jun 2, 2026

Subscript TagKeySerializer and TagValueSerializer with their already-typed response shapes, type the get_group_tag_key backend method's return, and tighten group_tagkey_details.py to Response[TagKeySerializerResponse].

Round 3 of the Response[T] rollout (after #116717 / #116736). Same no-cast principle: each tightening fixes the underlying source-typing gap rather than papering over it at the call site. The endpoint calls serialize(group_tag_key, request.user, serializer=TagKeySerializer()); without a typed return on get_group_tag_key, mypy resolved serialize() to the Any fallback overload and the body-is-Any plugin fired.

Typing the backend method's return as GroupTagKey | TagKey (the union the Snuba implementation already concretely returns) lets mypy pick the typed Serializer[T] overload and flow TagKeySerializerResponse through end-to-end.

… group_tagkey_details

Adds `Serializer[T]` subscripts to `TagKeySerializer` and `TagValueSerializer`
(both already had typed `serialize() -> ...Response` methods, only the base
class declaration was missing the parameter), and types the `get_group_tag_key`
backend method's return as `GroupTagKey | TagKey` in both the abstract base and
the Snuba implementation.

Round 3 of the Response[T] rollout (after #116717 / #116736). Tightening
`group_tagkey_details.py` to `Response[TagKeySerializerResponse]` required
fixing the source-typing chain end-to-end — the endpoint calls
`serialize(group_tag_key, request.user, serializer=TagKeySerializer())`, and
without a typed return on `get_group_tag_key`, mypy resolved `serialize()` to
the `Any` fallback overload and the body-is-Any plugin fired. Typing the
backend method lets mypy pick the typed `Serializer[T]` overload and flow
`TagKeySerializerResponse` through to the call site — no `cast()`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the Scope: Backend Automatically applied to PRs that change backend components label Jun 2, 2026
@azulus azulus marked this pull request as ready for review June 2, 2026 23:52
@azulus azulus requested review from a team as code owners June 2, 2026 23:52
…mentations

After typing `get_group_tag_key` to return `GroupTagKey | TagKey` in the
previous commit, mypy could see that `TagKey.top_values` was implicitly
`Any | None` — leaking through to test sites that iterate or index into
`.top_values` without a None check.

Type `TagKey.top_values: tuple[TagValue, ...] | None` (matching the
sibling `GroupTagKey.top_values: tuple[GroupTagValue, ...] | None`),
and narrow the `_ValueCallable[U]` Protocol's `value` parameter from
`object` to `str` (matches what `_make_result` actually passes — a key
from `result.items()` of type `dict[str, ...]`). The wider `object`
type was unsatisfiable by `TagValue.__init__` / `GroupTagValue.__init__`
(both take `value: str | None`); the cast at line 444 of snuba/backend.py
was a latent contravariance violation that only became visible after
typing the public-facing `get_group_tag_key` return.

Two test sites get `assert group_tag_key.top_values is not None` to
satisfy mypy after the typed-attribute narrowing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@azulus azulus merged commit f8d2a24 into master Jun 3, 2026
70 checks passed
@azulus azulus deleted the jeremy/migrate-tag-serializers-to-generic branch June 3, 2026 15:19
azulus added a commit that referenced this pull request Jun 3, 2026
Subscript `RelayUsageSerializer` with `OrganizationRelayResponse`, annotate
its `serialize()` return, and tighten `organization_relay_usage.py` to
`Response[list[OrganizationRelayResponse]]`. Pass the serializer explicitly
at the call site so mypy picks the typed `serialize(...)` overload instead
of the `Any` fallback.

Same pattern as #116743 (Tag(Key|Value)Serializer). No cast.
azulus added a commit that referenced this pull request Jun 3, 2026
Three patterns, source-typing only — no cast(), no behavior change:
annotation-only where the body matched the declared T; union with
ValidationErrorResponse + as_validation_errors() for the
`Response(serializer.errors, 400)` path; one Serializer[T] subscript on
ExternalActorSerializer to unlock typed-overload resolution.

Round 5 of the rollout — same shape as #116717 / #116736 / #116743.
azulus added a commit that referenced this pull request Jun 3, 2026
Three patterns, source-typing only — no cast(), no behavior change:
annotation-only where the body matched the declared T; union with
ValidationErrorResponse + as_validation_errors() for the
`Response(serializer.errors, 400)` path; one Serializer[T] subscript on
ExternalActorSerializer to unlock typed-overload resolution.

Round 5 of the rollout — same shape as #116717 / #116736 / #116743.
azulus added a commit that referenced this pull request Jun 3, 2026
Tighten 14 endpoints from `-> Response` to `-> Response[T]`.
Source-typing only — no `cast()`, no behavior change.

Three patterns: annotation-only where the body already matches the
declared T; `Response[T] | Response[ValidationErrorResponse]` with the
`as_validation_errors()` helper for endpoints whose 400 path returns
`serializer.errors`; one missing `Serializer[T]` subscript on
`ExternalActorSerializer` to unlock typed-overload resolution.

Round 5 of the rollout — same shape as #116717 / #116736 / #116743.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants