Fix two annotation-resolution regressions from the 3.13 rewrite#75
Merged
Conversation
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
This was referenced Jul 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
0.5.0/0.5.1were yanked. The "Support Python 3.13" annotation rework introduced two independent regressions that break everyfaust.Recorddownstream (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 classesannotations()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.Recordmatches 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'stest_models.pydefines its models._resolve_refs/eval_typealready resolve string/ForwardRef annotations to real types before this point, so the normalization pass was redundant for resolution and actively destructive.Fix: return
fieldsdirectly (as 0.4.x did)._normalize_forwardrefremains available as a comparison helper.Regression 2 —
eval_typerejects stringClassVarThe 3.14
ForwardRef-compat rewrite calls stdlibtyping._eval_typedirectly on theForwardRef, which runstyping._type_check— and that rejectsClassVar[...]withTypeError: ... is not valid as type argument. A stringClassVarannotation is legitimate (from __future__ import annotationsstringizes 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 aClassVar(callers drop it viaskip_classvar); otherwise re-raise the real error.Verification
test_annotations__preserves_function_local_class_identityandtest_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.faust-streaming/faustcheckout with this fixedmodeinstalled 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 fausttests/unit+tests/functional: 2128 passed, 32 skipped, 0 failed.🤖 Generated with Claude Code
https://claude.ai/code/session_01HgnKFtXZbCjXNoVNWa5JLd
Generated by Claude Code