Skip to content

Fix two annotation-resolution regressions from the 3.13 rewrite#75

Merged
wbarnha merged 1 commit into
masterfrom
claude/faust-mode-open-prs-ur3hqs
Jul 19, 2026
Merged

Fix two annotation-resolution regressions from the 3.13 rewrite#75
wbarnha merged 1 commit into
masterfrom
claude/faust-mode-open-prs-ur3hqs

Conversation

@wbarnha

@wbarnha wbarnha commented Jul 19, 2026

Copy link
Copy Markdown
Member

Description

0.5.0/0.5.1 were yanked. The "Support Python 3.13" annotation rework introduced two independent regressions that break every faust.Record downstream (they don't reproduce in mode's own tests, only downstream, which is why they slipped through both prior fixes).

Regression 1 — annotations() stringifies function-local classes

annotations() post-processed its result through _normalize_forwardref, which rewrites any type whose __qualname__ contains "<locals>" (i.e. every class defined inside a function) down to a bare __name__ string. faust.Record matches a field's resolved type by identity against its coercion mapping, so once the type becomes a string, coercion/polymorphic handling silently stops working for function-local model field types — which is exactly how faust's test_models.py defines its models. _resolve_refs/eval_type already resolve string/ForwardRef annotations to real types before this point, so the normalization pass was redundant for resolution and actively destructive.

Fix: return fields directly (as 0.4.x did). _normalize_forwardref remains available as a comparison helper.

Regression 2 — eval_type rejects string ClassVar

The 3.14 ForwardRef-compat rewrite calls stdlib typing._eval_type directly on the ForwardRef, which runs typing._type_check — and that rejects ClassVar[...] with TypeError: ... is not valid as type argument. A string ClassVar annotation is legitimate (from __future__ import annotations stringizes every annotation, ClassVars included), so any Record with a quoted ClassVar — or any Record at all under future-annotations — raised at class creation.

Fix: catch that TypeError, evaluate the forward arg directly, and accept the result when it's a ClassVar (callers drop it via skip_classvar); otherwise re-raise the real error.

Verification

  • Added test_annotations__preserves_function_local_class_identity and test_eval_type__string_classvar_is_not_rejected. Confirmed both fail against the genuine pre-fix code (checked out from the parent commit) with the exact errors described above, and pass with the fix.
  • mode's own suite: 738 passed on Python 3.11 and 3.13, ruff clean.
  • Cross-checked against a real faust-streaming/faust checkout with this fixed mode installed in place of the PyPI release, in a dedicated venv matching faust's own pinned test requirements: tests/functional/test_models.py — 89 passed, 0 failed (previously total collection failure). Full faust tests/unit + tests/functional: 2128 passed, 32 skipped, 0 failed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HgnKFtXZbCjXNoVNWa5JLd


Generated by Claude Code

Two independent regressions were introduced in the "Support Python 3.13"
annotation-handling rework and would break every faust.Record downstream:

1. `annotations()` returned field types mangled by `_normalize_forwardref`,
   which rewrites any type whose `__qualname__` contains "<locals>" -- i.e.
   every class defined inside a function -- down to a bare `__name__`
   *string*, destroying the actual class object. faust.Record matches a
   field's resolved type by identity against its coercion mapping, so this
   silently disabled coercion/polymorphic handling for function-local model
   field types (and broke faust's model test suite). `_resolve_refs`/
   `eval_type` already resolve string/ForwardRef annotations to real types,
   so no post-normalization is needed: return `fields` as-is (as 0.4.x did).
   `_normalize_forwardref` remains available as a comparison helper.

2. `eval_type` passed string annotations straight to `typing._eval_type`,
   whose `_type_check` rejects `ClassVar[...]` ("is not valid as type
   argument"). A *string* ClassVar is legitimate -- `from __future__ import
   annotations` stringizes every annotation -- so a Record with a quoted
   ClassVar (or any Record under future-annotations) raised at class
   creation. Catch that TypeError, evaluate the forward arg directly, and
   accept it when it is a ClassVar (callers drop it via skip_classvar).

Adds regression tests for both. Verified against faust: its full model/table
test suite goes from total collapse back to fully green, on Python 3.10-3.14.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHPL4VFWQRQPpjR1gXSKyL
@wbarnha
wbarnha merged commit 9511856 into master Jul 19, 2026
14 checks passed
@wbarnha
wbarnha deleted the claude/faust-mode-open-prs-ur3hqs branch July 19, 2026 04:49
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