feat[eve,next]: runtime PEP 695 type alias support and located annotation diagnostics - #2754
Merged
Conversation
'get_represented_types' is a third annotation-dispatch funnel alongside
'type_validation' and 'datamodels', but it was not wired into
'eval_type_alias'. A PEP 695 alias matched none of its branches and fell
through to the empty-tuple return, so the isinstance tuples built from it
('NUMERIC_VALUE_TYPES', 'PRIMITIVE_VALUE_TYPES', 'NAMED_COLLECTION_TYPES')
would silently become empty and every check against them constantly false.
Resolve aliases on entry, which also covers nesting since the generic
branches recurse through the same function. Update the ADR to list all
three funnels and add the first tests for this function.
'StringifyAnnotationsPass' replaced every annotation with a freshly built 'ast.Constant'. The pass runs after 'FixMissingLocations', so the new node carried no location at all and 'visit_AnnAssign' crashed with a bare 'AssertionError' when asking for it, breaking every annotated assignment inside a field operator. Copy the location of the original annotation node, which also makes the variable-annotation diagnostic point at the annotation instead of the whole statement. Add the missing diagnostic test for that path and a first end-to-end test of a PEP 695 alias used as a field operator annotation.
'_make_type_converter' evaluated the alias eagerly, so a coerced field annotated with an alias whose value is not resolvable yet failed at class creation, while the validator path two lines away already deferred it. Add a 'DeferredTypeConverter' mirroring 'ForwardRefValidator' so both paths follow the deferral contract described in the ADR. Also declare 'typing_extensions.TypeAliasType' in 'SingleTypeAnnotation', which was missing even though the runtime helpers accept it; report the original annotation instead of an internal link in the alias depth-limit error; and propagate 'globalns'/'localns' through the recursive calls of 'get_represented_types', which were resolving nested forward references against the wrong scope.
egparedes
commented
Aug 10, 2026
egparedes
left a comment
Contributor
Author
There was a problem hiding this comment.
A couple of questions
The value of a PEP 695 alias is arbitrary user code evaluated on first
access, so it can fail in any way, but only 'NameError' was handled. A
typo'd dtype ('type F = Field[Dims[I], np.foat64]') raised 'AttributeError'
straight through 'eval_type_alias', '_resolve_type_alias' and
'type_from_annotation' to the user, with no source location and no
diagnostic.
Wrap every non-'NameError' failure of the alias value into a 'TypeError'
naming the alias and the cause. 'NameError' stays unwrapped because it is
the deferral signal: it means the name is not defined _yet_, while any
other failure will never resolve and so must be reported where it is found
instead of at a later instantiation. Consumers pass the message through
verbatim rather than re-wording it, as it is the only text naming the
actual cause; on the frontend it now reaches the user as an
'InvalidAnnotationError' with the carets under the annotation.
A caret-run regex without a trailing lookahead ('\^{13}') also matches any
longer run, so it matched the 63-caret whole-function span it was meant to
rule out and pinned nothing. Assert on the 'SourceLocation' columns, which
is renderer-independent, and add the lookahead.
Doing so showed the parameter diagnostic covers 'a: BrokenFieldAlias', not
the annotation alone: 'visit_arg' locates the whole 'ast.arg'. Fix the
comment to describe what the code actually does.
Both return-annotation diagnostics used the location of the whole function definition, so a bad annotation underlined the entire signature and body instead of the offending span, against rule 2 of the error-message guide. Validate the annotation in 'visit_FunctionDef', where the 'ast' node it was written at is still available. The check stays split rather than moved, because '_postprocess_dialect_ast' compares the annotated type against the deduced one, which does not exist until the body has been typed. Only the location is taken from the ast; the value still comes from 'annotations', so the stringified annotation is never re-evaluated. Point the annotated-vs-deduced mismatch at the returned expression, which is where the deduced type comes from, falling back to the function when there is more than one return statement. Note that a bad return annotation is now reported before body type deduction errors, which is the opposite of the previous order.
The depth bound alone could not tell a cycle from a legitimately deep
chain, so 'type A = A' and a mutual 'A -> B -> A' were both reported as
"recursive or nested too deeply".
Keep the set of aliases already walked through: a cycle repeats one, so it
is detected as soon as an alias is seen twice, however long the cycle is.
The bound stays as the backstop it has to be, since a parametrized alias
which grows on every step ('type G[T] = G[Tuple[T]]') builds a bigger
annotation each time and never repeats one.
Note that tracking 'id()' instead of the objects would be unsound here:
the intermediate annotations are temporaries, and a freed id gets reused,
which reports a cycle where there is none.
Keeping the rationale in 'docs/development/ADRs/eve/' put it a directory
tree away from the three functions it constrains, where a reader touching
'eval_type_alias' would not find it.
Move the parts that are not evident from the code into comments next to
what they explain: why aliases are resolved at the dispatch funnels rather
than by rewriting stored annotations, that a funnel left out fails silently
by returning an empty tuple, why both alias classes have to be checked and
why 'hasattr(obj, "__value__")' is not equivalent, why 'NameError' is a
deferral signal while every other failure is permanent, and the two known
limits ('ClassVar' behind an alias, and aliases used as runtime values,
which is what keeps ruff's 'UP040' disabled).
Drop the rest: the context section restates PEP 695, and the rejected
alternatives are not needed to work on the code.
This removes the whole 'ADRs/eve' hierarchy, which was added by this same
branch and holds no other decision. Nothing links to it.
egparedes
commented
Aug 10, 2026
Halve the folded-in rationale (46 lines out, 23 in), keeping only what is not evident from the code: resolution at the dispatch funnels and why not eagerly, that a missed funnel fails silently, the 'hasattr' trap, the 'NameError'-defers / everything-else-fails split, and the two unsupported cases.
egparedes
requested review from
havogt and
romanc
and removed request for
romanc
August 10, 2026 13:07
havogt
reviewed
Aug 10, 2026
Three points from @havogt's review: - The deferral rationale was repeated, with small variations, at four call sites. Keep it once in 'eval_type_alias', where the signal originates, and leave one-line pointers at the sites that react to it. - The return-annotation check in 'visit_FunctionDef' discarded the result of 'type_from_annotation', which read as a mistake. Name it for what it does, '_reject_invalid_return_annotation', and explain in its docstring why the translation happens twice: '_postprocess_dialect_ast' needs the value to compare against the deduced type, but can no longer report a failure, since by then the ast is gone and it could only point at the whole function. - '_returned_value_location' used neither 'cls' nor 'self'. Make it a module level helper, as 'dialect_parser' does with '_describe_unsupported_feature'. No behavior change; error messages, spans and tests are untouched.
The lockfile update on main (#2746) moved ruff to 0.16.1, which flags 'implicit-string-concatenation-in-collection-literal' on the hint tuple in 'InvalidAnnotationError'. Wrap the two fragments, as the rule suggests. Found only in CI because 'pull_request' runs check the merge with the current base, so they were already using the new ruff while this branch was still on 0.15.7.
egparedes
added a commit
that referenced
this pull request
Aug 13, 2026
Three files conflicted, all against #2772 and #2754. 'gtscript_frontend.py': #2772 removed the same deprecated 'ast' usage this branch fixes, but replaced the ellipsis check with 'isinstance(cn, types.EllipsisType)', which never matches — that is the type of the '...' object, not of its AST node ('ast.Constant' with an 'Ellipsis' value). Kept this branch's '_is_ellipsis_node' helper, which does match, and took #2772's better 'visit_Expr': it drops only *string* constant statements rather than every constant, matching the documented intent. The two cartesian test hunks were pure additions on both sides (the ellipsis tests here, the IntEnum tests from #2323) and are both kept. 'test_diagnostic_messages.py': #2754 added PEP 695 alias tests; this branch deleted 'test_add_note_folded_into_str_on_py310', whose 'skipif(version_info >= (3, 11))' means it runs on no supported version. Both intents are preserved, which leaves 'import sys' unused — dropped. #2754 also added test cases spelled with the deprecated 'typing' aliases this branch removes ('List[SampleIntAlias]', 'type SampleGenericAlias[T] = Tuple[T, T]', ...). Those are migrated to the builtin spelling, which git could not do since the two changes touch different lines.
egparedes
added a commit
that referenced
this pull request
Aug 13, 2026
'_is_strictly_immutable_type' is a fourth annotation-dispatch funnel, added on this branch after #2754 landed runtime PEP 695 support, so it did not learn to resolve aliases. A 'type MyPair = tuple[int, int]' field was rejected while the identical plain annotation was accepted — conservative, but it makes 'frozen="strict"' unusable with the alias feature that just landed, and an alias hiding a mutable type was only rejected by accident. Resolve the alias first and check what it stands for, so parametrized aliases ('PairAlias[int]') are decomposed like any other annotation. An alias whose value cannot be evaluated — undefined name, recursive, not parametrizable — proves nothing about immutability and is rejected like any other unresolved annotation, rather than escaping as the raw 'NameError' / 'TypeError' from 'eval_type_alias'. The new tests define their aliases at module level: this file uses PEP 563, so a function-local alias would only ever be seen as an unresolvable forward reference and the tests would pass for the wrong reason.
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
Python 3.12 (PEP 695)
type X = ...binds atyping.TypeAliasTyperather thanthe annotation itself, and nothing unwraps it at runtime:
get_type_hintsreturns it as is,
__value__is lazy and raisesNameErroruntil the names itmentions exist, and a plain
isinstance(x, xtyping.TypeAliasType)missesnative aliases because
eve.extended_typingstar-imports bothtypingandtyping_extensions. Now that the floor is 3.12, users write these in datamodelfields and DSL signatures.
eve.extended_typingexposesis_type_alias()andeval_type_alias(),and resolution happens at the three annotation-dispatch funnels
(
type_validation.SimpleTypeValidatorFactory.__call__,datamodels.core._make_type_converter,get_represented_types) rather than atannotation-storage time, so nesting (
list[MyAlias]) is covered for free andthe stored annotation keeps the alias name for reprs and signatures.
Failures split on whether the annotation can still become valid.
NameErrordefers to first instantiation via
ForwardRefValidator, or the newDeferredTypeConverterfor coerced fields, exactly as string forwardreferences already do. Anything else is permanent — an alias value is arbitrary
user code, so a typo'd dtype raises
AttributeError— and is wrapped into aTypeErrornaming the alias and the cause. Cycles are detected by the set ofvisited aliases, with the depth cap (64) kept as the backstop for aliases that
grow on every step (
type G[T] = G[Tuple[T]]).next.from_type_hintresolves aliases, and annotation failures reachthe user as a located
InvalidAnnotationErrorinstead of a bareValueErroror a raw traceback, through a shared
type_from_annotation()indialect_parser. Two span defects are fixed:StringifyAnnotationsPassleftrebuilt annotations with no location at all, crashing
visit_AnnAssignonevery annotated assignment inside a field operator; and both return-annotation
diagnostics pointed at the whole function definition. This reorders one case —
a bad return annotation is now reported before body type-deduction errors.
For reviewers
get_represented_typesfails silently: an unresolved alias matches none ofits branches and falls through to its empty-tuple return, which would quietly
empty the import-time isinstance tuples in
next/common.pyandnext/named_collections.py. It had no test coverage before this PR.DSLTypeErrorloses its__init__. That override was a pass-through with anarrower signature than
DSLError, so removing it only widens what isaccepted; it is required for subclasses to pass
label/notes/hints.(
common.Tag, subclassed atiterator/embedded.py:102) cannot be rewrittento
type X = ...at all, so ruff'sUP040stays off repo-wide. PEP 695generics already worked and are unchanged.
Requirements
eve/extended_typing.pyinstead; theADRs/eve/hierarchy added earlieron this branch was removed.