[Type] ndarray typing 1: Add eval_str=True to inspect.signature() calls#411
[Type] ndarray typing 1: Add eval_str=True to inspect.signature() calls#411hughperkins merged 14 commits intomainfrom
Conversation
…annotations Support `from __future__ import annotations` by resolving stringified type annotations at signature inspection time. Catch NameError from invalid string annotations and raise a clear QuadrantsSyntaxError.
|
Opus 4.6 review: PR Review: hp/typing-t4-1-eval-strSummaryThis PR adds Issues Found1. Inconsistent
|
The register decorator's inspect.signature() call was missing eval_str=True, inconsistent with all other call sites.
Wrap all inspect.signature(eval_str=True) calls with try/except that catches both NameError and AttributeError, re-raising as QuadrantsSyntaxError for clear user-facing errors. Previously only check_parameter_annotations had error handling and it only caught NameError.
Centralize the inspect.signature(eval_str=True) + error handling pattern into a shared get_func_signature() helper in exception.py, replacing 5 inline try/except blocks across _func_base.py, _kernel_impl_dataclass.py, and _perf_dispatch.py.
Verify that kernel parameter annotations are correctly resolved when the module uses PEP 563 stringified annotations.
Pylint flagged C0415 (import-outside-toplevel) for the local inspect import in exception.py and the local get_func_signature import in _kernel_impl_dataclass.py. Hoist both to module top-level imports. Made-with: Cursor
Made-with: Cursor # Conflicts: # python/quadrants/lang/_perf_dispatch.py
- Move get_func_signature out of exception.py into a new quadrants.lang._signature module so the helper lives somewhere whose name actually describes its purpose. - Add explicit type annotations (Callable -> inspect.Signature); the new module is not blanket-`# type: ignore`d so the signature is type-checked. - Generalize the error message from "Invalid type annotation of Taichi kernel" to "Invalid type annotation in `<func.__qualname__>`" since the helper is also used by @perf_dispatch and non-kernel FuncBase paths. - Update import sites in _func_base, _kernel_impl_dataclass, and _perf_dispatch to pull from the new module.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 74936177bc
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| """ | ||
| try: | ||
| return inspect.signature(func, eval_str=True) | ||
| except (NameError, AttributeError) as e: |
There was a problem hiding this comment.
Normalize all eval_str annotation errors to QuadrantsSyntaxError
get_func_signature() only catches NameError and AttributeError around inspect.signature(..., eval_str=True), but annotation evaluation can also raise other exceptions (for example SyntaxError from a quoted annotation like "broken[" or TypeError from invalid typing expressions). In those cases registration now leaks raw Python exceptions instead of the intended QuadrantsSyntaxError, which regresses error consistency for kernel/perf_dispatch annotation failures.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
LGTM — the previous review's concerns have all been addressed; the remaining gap in is a known nit already called out by the author.
Extended reasoning...
Overview
This PR centralizes all calls behind a new helper in , adding to correctly resolve stringified annotations (PEP 563 / ). Five call sites across , , and are migrated; a regression test is added.
Security risks
None. This is purely an introspection/annotation-evaluation change with no security-sensitive code paths (no auth, crypto, user input parsing, or permissions logic involved).
Level of scrutiny
Moderate: the change touches kernel registration paths that run on every / / decoration, but only the one-time registration step (not the hot call path). The diff is small (+43/−8) and all modifications are mechanical substitutions of → .
Other factors
All substantive issues raised in the previous Opus 4.6 review have been resolved: now uses ; the helper lives in a dedicated ; the error message is generic. The one outstanding item — not being caught alongside / — is a known, author-acknowledged weakness flagged in the PR description and captured as a [Nit] inline comment. It does not affect correctness for the intended use case and can be addressed in a follow-up.
| try: | ||
| return inspect.signature(func, eval_str=True) | ||
| except (NameError, AttributeError) as e: | ||
| qualname = getattr(func, "__qualname__", repr(func)) | ||
| raise QuadrantsSyntaxError(f"Invalid type annotation in `{qualname}`: {e}") from e |
There was a problem hiding this comment.
🟡 The except clause in get_func_signature only catches (NameError, AttributeError), but inspect.signature(func, eval_str=True) can also raise SyntaxError for syntactically invalid string annotations (e.g. a: 'NDArray['). Such errors propagate as raw Python tracebacks instead of QuadrantsSyntaxError, violating the docstring's promise to convert annotation-evaluation failures into user-friendly errors. Fix by adding SyntaxError (and optionally TypeError) to the except tuple.
Extended reasoning...
What the bug is and how it manifests
The new get_func_signature helper in python/quadrants/lang/_signature.py wraps inspect.signature(func, eval_str=True) to resolve stringified annotations. Its docstring explicitly promises: "Annotation-evaluation failures (NameError, AttributeError) are re-raised as QuadrantsSyntaxError". However, the except clause only covers those two exception types, leaving SyntaxError (and others like TypeError) unhandled.
The specific code path that triggers it
At _signature.py lines 19–23`:
try:
return inspect.signature(func, eval_str=True)
except (NameError, AttributeError) as e:
qualname = getattr(func, "__qualname__", repr(func))
raise QuadrantsSyntaxError(f"Invalid type annotation in `{qualname}`: {e}") from eWhen a user writes a kernel with a syntactically broken string annotation (e.g. a: 'NDArray['), Python's annotation evaluation calls eval() on the string, which raises SyntaxError: '[' was never closed. This exception is not in the except tuple and propagates directly to the caller as a raw Python exception.
Why existing code doesn't prevent it
There is no broader try/except in the callers (_func_base.py, _kernel_impl_dataclass.py, _perf_dispatch.py) that would intercept this before it reaches the user. The entire point of the helper was to be that single chokepoint for clean error translation — but it has a gap.
What the impact would be
Any user who writes a kernel or @perf_dispatch function with a syntactically invalid string annotation will receive a raw SyntaxError traceback originating from deep inside inspect rather than a QuadrantsSyntaxError with a readable message. This is a documented contract violation. The PR author themselves acknowledged this as a known weakness in the "Bad / weak points" section of the PR description.
How to fix it
Add SyntaxError (and optionally TypeError) to the except tuple:
except (NameError, AttributeError, SyntaxError, TypeError) as e:Step-by-step proof
- User defines a kernel with an invalid string annotation:
@qd.kernel def bad_kernel(a: 'NDArray['): pass
- During kernel registration,
FuncBase.__init__callsself.check_parameter_annotations(). check_parameter_annotationscallsget_func_signature(self.func)at_signature.py:19.inspect.signature(bad_kernel, eval_str=True)internally callseval("NDArray["), which raisesSyntaxError: '[' was never closed.- The
except (NameError, AttributeError)clause does NOT matchSyntaxError. - The
SyntaxErrorpropagates unhandled up the call stack to the user, showing a raw traceback from insideinspectinstead of aQuadrantsSyntaxError.
Empirically verified: python3 -c "def f(a: 'NDArray['): pass; import inspect; inspect.signature(f, eval_str=True)" raises SyntaxError, confirming the gap.
inspect.signature(func, eval_str=True) can raise SyntaxError for syntactically invalid string annotations (e.g. `a: "NDArray["`) and TypeError for annotations that cannot be evaluated as types. These were previously leaking as raw Python tracebacks instead of being wrapped as QuadrantsSyntaxError, contradicting the helper's docstring.
inspect.signature itself raises TypeError for non-introspectable objects (some C built-ins, etc.), so wrapping that as "invalid type annotation" would be misleading. Narrow the except tuple back to (NameError, AttributeError, SyntaxError) and document why TypeError is excluded.
…annotations
Summary
Enables
quadrantskernels and@perf_dispatchfunctions to work correctly when their type annotations are stringified — either explicitly (a: "qd.types.NDArray[qd.i32, 1]") or implicitly viafrom __future__ import annotations(PEP 563).Previously,
inspect.signature(func)returned annotations as raw strings, which broke downstream code that expected real type objects (e.g.is_dataclass(parameter.annotation)silently returnedFalsefor stringified dataclass annotations, andNDArray-style annotations were not recognized).Changes
get_func_signature(func)inpython/quadrants/lang/exception.py:inspect.signature(func, eval_str=True)so annotations are resolved to real objects.NameError/AttributeErrorraised during annotation evaluation and re-raises asQuadrantsSyntaxErrorwith a clear message, so users get a Taichi-flavorederror instead of a raw Python traceback.
python/quadrants/lang/_func_base.py(2 sites:FuncBaseregistration + dataclass template arg expansion)python/quadrants/lang/_kernel_impl_dataclass.py(1 site:extract_struct_locals_from_context)python/quadrants/lang/_perf_dispatch.py(2 sites:PerformanceDispatcher.__init__+ theregisterdecorator)tests/python/test_future_annotations.pyadds an end-to-end test of a kernel withNDArray[qd.i32, 1]annotations underfrom __future__ import annotations.Good points
from __future__ import annotations(which is increasingly common / will eventually be the default) silently breaksdataclass and ndarray kernel arguments.
eval_str=True+ error translation in one helper means future signature-introspection sites only have one obvious thing to call, and theerror-conversion behavior stays consistent.
QuadrantsSyntaxError("Invalid type annotation of Taichi kernel: ...")instead of a bareNameErrorfrom deepinside
inspect.import inspectis removed from modules that no longer need it directly.Bad / weak points (worth reviewer attention)
Helper lives in
[fixed]exception.py.get_func_signatureis a signature utility, not an exception class. It was placed there to avoid an import cycle with_func_base/_perf_dispatch, butexception.pyis now misnamed for part of its contents. A dedicated_signature.py(or putting it inutil.py) would be cleaner if the import graphallows.
exceptclause. OnlyNameErrorandAttributeErrorare caught.inspect.signature(..., eval_str=True)can also surfaceSyntaxError,TypeError, orarbitrary exceptions from
__getattr__/ forward-ref resolution, which will still bubble up as raw tracebacks. Consider broadening toException(with the original chainedvia
from e) or documenting why only these two.Error message hard-codes "Taichi". The message says "Invalid type annotation of Taichi kernel", but the helper is also used by
[fixed]@perf_dispatchand by non-kernelFuncBasepaths. Message should probably be generic ("Invalid type annotation in{func.__qualname__}: ...") or context-specific per call site.[fixed]# type: ignoreat the top ofexception.pyis preserved. The new helper inherits that blanket ignore, so type errors inget_func_signatureitself won't be caught bythe type checker. Worth either typing the helper explicitly (
def get_func_signature(func: Callable) -> inspect.Signature) or moving it to a typed module.eval_str=Trueis a no-op for annotations that are already real objects, but it does trigger evaluation for anystring annotation a user may have written intentionally (e.g. forward references to types that only exist at runtime in another scope). Anyone currently relying on getting raw
strings back from these signatures will see a behavior change. Low risk, but worth calling out in release notes.
NDArrayunderfrom __future__ import annotations). Missing coverage for:_kernel_impl_dataclass.py)@perf_dispatchregister / dispatch with stringified annotationsQuadrantsSyntaxError, notNameErrormain. History could be rebased before merge for a cleaner log (6 logical commits + 2 merges → 6 commits).Issue: #
Brief Summary
copilot:summary
Walkthrough
copilot:walkthrough