Skip to content

[Type] ndarray typing 1: Add eval_str=True to inspect.signature() calls#411

Merged
hughperkins merged 14 commits intomainfrom
hp/typing-t4-1-eval-str
Apr 23, 2026
Merged

[Type] ndarray typing 1: Add eval_str=True to inspect.signature() calls#411
hughperkins merged 14 commits intomainfrom
hp/typing-t4-1-eval-str

Conversation

@hughperkins
Copy link
Copy Markdown
Collaborator

@hughperkins hughperkins commented Mar 12, 2026

…annotations

Summary

Enables quadrants kernels and @perf_dispatch functions to work correctly when their type annotations are stringified — either explicitly (a: "qd.types.NDArray[qd.i32, 1]") or implicitly via from __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 returned False for stringified dataclass annotations, and NDArray-style annotations were not recognized).

Changes

  • New helper get_func_signature(func) in python/quadrants/lang/exception.py:
    • Wraps inspect.signature(func, eval_str=True) so annotations are resolved to real objects.
    • Catches NameError / AttributeError raised during annotation evaluation and re-raises as QuadrantsSyntaxError with a clear message, so users get a Taichi-flavored
      error instead of a raw Python traceback.
  • Call-site migration — all 5 call sites that introspect a user kernel/func signature now go through the helper:
    • python/quadrants/lang/_func_base.py (2 sites: FuncBase registration + 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__ + the register decorator)
  • Testtests/python/test_future_annotations.py adds an end-to-end test of a kernel with NDArray[qd.i32, 1] annotations under from __future__ import annotations.

Good points

  • Fixes a real correctness bug. Without this, from __future__ import annotations (which is increasingly common / will eventually be the default) silently breaks
    dataclass and ndarray kernel arguments.
  • Single chokepoint. Centralizing eval_str=True + error translation in one helper means future signature-introspection sites only have one obvious thing to call, and the
    error-conversion behavior stays consistent.
  • User-facing error quality. A bad annotation now raises QuadrantsSyntaxError("Invalid type annotation of Taichi kernel: ...") instead of a bare NameError from deep
    inside inspect.
  • Small, focused diff (+43 / -8 across 5 files) with a dedicated regression test.
  • Clean import hygieneimport inspect is removed from modules that no longer need it directly.

Bad / weak points (worth reviewer attention)

  • Helper lives in exception.py. get_func_signature is a signature utility, not an exception class. It was placed there to avoid an import cycle with _func_base /
    _perf_dispatch, but exception.py is now misnamed for part of its contents. A dedicated _signature.py (or putting it in util.py) would be cleaner if the import graph
    allows.
    [fixed]
  • Narrow except clause. Only NameError and AttributeError are caught. inspect.signature(..., eval_str=True) can also surface SyntaxError, TypeError, or
    arbitrary exceptions from __getattr__ / forward-ref resolution, which will still bubble up as raw tracebacks. Consider broadening to Exception (with the original chained
    via 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 @perf_dispatch and by non-kernel
    FuncBase paths. Message should probably be generic ("Invalid type annotation in {func.__qualname__}: ...") or context-specific per call site.
    [fixed]
  • # type: ignore at the top of exception.py is preserved. The new helper inherits that blanket ignore, so type errors in get_func_signature itself won't be caught by
    the type checker. Worth either typing the helper explicitly (def get_func_signature(func: Callable) -> inspect.Signature) or moving it to a typed module.
    [fixed]
  • Behavioral change for already-evaluated annotations. eval_str=True is a no-op for annotations that are already real objects, but it does trigger evaluation for any
    string 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.
  • Test coverage is thin. Only one happy-path test (NDArray under from __future__ import annotations). Missing coverage for:
    • dataclass arguments with stringified annotations (the main motivating case in _kernel_impl_dataclass.py)
    • @perf_dispatch register / dispatch with stringified annotations
    • the error path — that a bad annotation produces QuadrantsSyntaxError, not NameError
  • Two merge commits from main. 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

…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.
@hughperkins
Copy link
Copy Markdown
Collaborator Author

Opus 4.6 review:

PR Review: hp/typing-t4-1-eval-str

Summary

This PR adds eval_str=True to all inspect.signature() calls in the kernel/func registration path, enabling proper evaluation of string annotations (e.g. from from __future__ import annotations). It also wraps the primary call in _func_base.check_parameter_annotations with a try/except that converts NameError (undefined type in annotation) into QuadrantsSyntaxError for clearer user feedback.

Issues Found

1. Inconsistent inspect.signature usage in _perf_dispatch.py

Location: python/quadrants/lang/_perf_dispatch.py, line 102

The PerformanceDispatcher.register decorator still uses inspect.signature(func) without eval_str=True:

def decorator(func: Callable | QuadrantsCallable) -> DispatchImpl:
    sig = inspect.signature(func)  # <-- missing eval_str=True

The constructor (line 61) uses eval_str=True for the prototype function, but registered implementations do not. For consistency and to handle from __future__ import annotations in registered implementations, this call should also use eval_str=True. If added, it should be wrapped in the same try/except NameError pattern for consistent error handling when annotations reference undefined types.

2. Missing error handling at other call sites

Locations:

  • _func_base.py line 195: _populate_global_vars_for_templates
  • _kernel_impl_dataclass.py line 76: extract_struct_locals_from_context

These calls use inspect.signature(..., eval_str=True) but do not catch NameError. If a kernel/func has an invalid type annotation (e.g. typo in a dataclass parameter), these will raise a raw NameError instead of QuadrantsSyntaxError. The error handling in check_parameter_annotations is good, but these downstream paths can still surface NameError before or in different code paths. Consider adding the same try/except at these sites for consistent user-facing errors.

3. Narrow exception handling

Location: _func_base.py lines 100–103

The PR only catches NameError. Per Python docs, when eval_str=True, the eval() used to resolve annotations "could potentially raise any kind of exception" (e.g. TypeError, AttributeError). Catching only NameError is reasonable for the common "undefined type" case, but other annotation evaluation failures would still propagate as raw exceptions. This is a minor point—NameError is likely the dominant case—but worth noting.

Suggestions for Improvement

  1. Add eval_str=True to _perf_dispatch.py line 102 for consistency with the rest of the codebase. If the registered function has invalid annotations, wrap in try/except and re-raise as QuadrantsSyntaxError for consistent UX.

  2. Consider a small helper to centralize the inspect.signature(..., eval_str=True) + NameError handling, e.g.:

    def get_signature_with_eval(func):
        try:
            return inspect.signature(func, eval_str=True)
        except NameError as e:
            raise QuadrantsSyntaxError(f"Invalid type annotation: {e}") from e

    This would reduce duplication and ensure consistent behavior across all call sites.

  3. Add a test for from __future__ import annotations with a kernel that uses type hints (e.g. NDArray, dataclass params) to lock in the intended behavior and guard against regressions.

Style & Correctness

  • Correctness: The change correctly enables string annotation evaluation. Existing tests (test_kernel_bad_argument_annotation, test_func_bad_argument_annotation) pass and continue to expect QuadrantsSyntaxError for invalid annotations.
  • Style: Matches existing patterns (e.g. QuadrantsSyntaxError usage, from e in re-raise).
  • Python version: eval_str is available in Python 3.10+; the project supports 3.10–3.13, so compatibility is fine.

Verdict

The PR achieves its goal and improves support for from __future__ import annotations. The main follow-ups are consistency (the _perf_dispatch call site) and optional hardening of error handling at the other eval_str=True call sites.

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.
@hughperkins hughperkins marked this pull request as ready for review March 12, 2026 04:55
@hughperkins hughperkins marked this pull request as draft March 12, 2026 04:59
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.
@hughperkins hughperkins marked this pull request as ready for review April 20, 2026 08:40
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread python/quadrants/lang/_signature.py Outdated
"""
try:
return inspect.signature(func, eval_str=True)
except (NameError, AttributeError) as e:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +19 to +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 e
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 e

When 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

  1. User defines a kernel with an invalid string annotation:
    @qd.kernel
    def bad_kernel(a: 'NDArray['): pass
  2. During kernel registration, FuncBase.__init__ calls self.check_parameter_annotations().
  3. check_parameter_annotations calls get_func_signature(self.func) at _signature.py:19.
  4. inspect.signature(bad_kernel, eval_str=True) internally calls eval("NDArray["), which raises SyntaxError: '[' was never closed.
  5. The except (NameError, AttributeError) clause does NOT match SyntaxError.
  6. The SyntaxError propagates unhandled up the call stack to the user, showing a raw traceback from inside inspect instead of a QuadrantsSyntaxError.

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.
@hughperkins hughperkins merged commit ee425d5 into main Apr 23, 2026
47 checks passed
@hughperkins hughperkins deleted the hp/typing-t4-1-eval-str branch April 23, 2026 06:28
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.

1 participant