From 238fdc665d5d8766b2582630095df7341be9eb30 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Fri, 23 Jan 2026 15:14:34 -0800 Subject: [PATCH 1/3] Deal with from __future__ import annotations. --- typemap/type_eval/_apply_generic.py | 28 ++++++++++++++++--- typemap/type_eval/_eval_call.py | 3 ++- typemap/type_eval/_eval_operators.py | 40 +++++++++++++++++++++++++++- 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/typemap/type_eval/_apply_generic.py b/typemap/type_eval/_apply_generic.py index 2f2eb4d..65e739a 100644 --- a/typemap/type_eval/_apply_generic.py +++ b/typemap/type_eval/_apply_generic.py @@ -1,6 +1,7 @@ import annotationlib import dataclasses import inspect +import sys import types import typing @@ -68,7 +69,9 @@ def dump(self, *, _level: int = 0): def substitute(ty, args): if ty in args: return args[ty] - elif isinstance(ty, (typing_GenericAlias, types.GenericAlias)): + elif isinstance( + ty, (typing_GenericAlias, types.GenericAlias, types.UnionType) + ): return ty.__origin__[*[substitute(t, args) for t in ty.__args__]] else: return ty @@ -240,8 +243,27 @@ def get_local_defns(boxed: Boxed) -> tuple[dict[str, Any], dict[str, Any]]: else: annos[k] = v elif af := getattr(boxed.cls, "__annotations__", None): - # TODO: substitute vars in this case - annos.update(af) + _globals = {} + if mod := sys.modules.get(boxed.cls.__module__): + _globals.update(vars(mod)) + _globals.update(boxed.str_args) + + _locals = dict(boxed.cls.__dict__) + _locals.update(boxed.str_args) + + for k, v in af.items(): + if isinstance(v, str): + result = eval(v, _globals, _locals) + # Handle cases where annotation is explicitly a string + # e.g. + # class Foo[T]: + # x: "Bar[T]" + if isinstance(result, str): + result = eval(result, _globals, _locals) + annos[k] = result + + else: + annos[k] = v for name, orig in boxed.cls.__dict__.items(): if name in EXCLUDED_ATTRIBUTES: diff --git a/typemap/type_eval/_eval_call.py b/typemap/type_eval/_eval_call.py index 8246b39..0da09d3 100644 --- a/typemap/type_eval/_eval_call.py +++ b/typemap/type_eval/_eval_call.py @@ -39,7 +39,8 @@ def _get_bound_type_args( arg_types: tuple[RtType, ...], kwarg_types: dict[str, RtType], ) -> dict[str, RtType]: - sig = inspect.signature(func) + sig = _eval_operators._resolved_function_signature(func) + bound = sig.bind(*arg_types, **kwarg_types) return { diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 3842b2d..c74d18a 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -458,7 +458,7 @@ def _callable_type_to_method(name, typ): def _function_type(func, *, receiver_type): root = inspect.unwrap(func) - sig = inspect.signature(root) + sig = _resolved_function_signature(root) # XXX: __type_params__!!! empty = inspect.Parameter.empty @@ -513,6 +513,44 @@ def _ann(x): return f +def _resolved_function_signature(func): + """Get the signature of a function with type hints resolved. + + This is used to deal with string annotations in the signature which are + generated when using __future__ import annotations. + """ + + sig = inspect.signature(func) + + _globals, _locals = _get_function_hint_namespaces(func) + if hints := typing.get_type_hints( + func, globalns=_globals, localns=_locals, include_extras=True + ): + params = [] + for name, param in sig.parameters.items(): + annotation = hints.get(name, param.annotation) + params.append(param.replace(annotation=annotation)) + + return_annotation = hints.get("return", sig.return_annotation) + sig = sig.replace( + parameters=params, return_annotation=return_annotation + ) + + return sig + + +def _get_function_hint_namespaces(func): + globalns = {} + localns = {} + + # module globals + module = inspect.getmodule(func) + if module: + globalns |= vars(module) + + return globalns, localns + + def _hints_to_members(hints, ctx): """Convert a hints dictionary to a tuple of Member types.""" return tuple[ From 4a1e9449fdd8b0970dd9c7c683218d4a090d8bd0 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Fri, 23 Jan 2026 18:38:05 -0800 Subject: [PATCH 2/3] Try and get namespaces for type hints. --- typemap/type_eval/_eval_operators.py | 33 +++++++++++++---- typemap/type_eval/_eval_typing.py | 53 +++++++++++++++++++++++++++- 2 files changed, 79 insertions(+), 7 deletions(-) diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index c74d18a..fb9cfb4 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -11,7 +11,10 @@ from typemap import type_eval from typemap.type_eval import _apply_generic, _typing_inspect -from typemap.type_eval._eval_typing import _eval_types +from typemap.type_eval._eval_typing import ( + _eval_types, + _get_class_type_hint_namespaces, +) from typemap.typing import ( Attrs, Capitalize, @@ -458,7 +461,7 @@ def _callable_type_to_method(name, typ): def _function_type(func, *, receiver_type): root = inspect.unwrap(func) - sig = _resolved_function_signature(root) + sig = _resolved_function_signature(root, receiver_type) # XXX: __type_params__!!! empty = inspect.Parameter.empty @@ -513,7 +516,7 @@ def _ann(x): return f -def _resolved_function_signature(func): +def _resolved_function_signature(func, receiver_type=None): """Get the signature of a function with type hints resolved. This is used to deal with string annotations in the signature which are @@ -522,7 +525,7 @@ def _resolved_function_signature(func): sig = inspect.signature(func) - _globals, _locals = _get_function_hint_namespaces(func) + _globals, _locals = _get_function_hint_namespaces(func, receiver_type) if hints := typing.get_type_hints( func, globalns=_globals, localns=_locals, include_extras=True ): @@ -539,14 +542,32 @@ def _resolved_function_signature(func): return sig -def _get_function_hint_namespaces(func): +def _get_function_hint_namespaces(func, receiver_type=None): globalns = {} localns = {} # module globals module = inspect.getmodule(func) if module: - globalns |= vars(module) + globalns |= module.__dict__ + + # If no receiver was specified, this might still be a method, try to get + # the class from the qualname. + if ( + not receiver_type + and (qn := getattr(func, '__qualname__', None)) + and '.' in qn + ): + class_name = qn.rsplit('.', 1)[0] + receiver_type = getattr(module, class_name, None) + + # Get the class's type hint namespaces + if receiver_type: + cls_globalns, cls_localns = _get_class_type_hint_namespaces( + receiver_type + ) + globalns.update(cls_globalns) + localns.update(cls_localns) return globalns, localns diff --git a/typemap/type_eval/_eval_typing.py b/typemap/type_eval/_eval_typing.py index 73b17f5..2db3fd7 100644 --- a/typemap/type_eval/_eval_typing.py +++ b/typemap/type_eval/_eval_typing.py @@ -21,7 +21,7 @@ if typing.TYPE_CHECKING: from typing import Any -from . import _apply_generic +from . import _apply_generic, _typing_inspect __all__ = ("eval_typing",) @@ -272,8 +272,59 @@ def _eval_func( return _apply_generic.make_func(func, annos) +def _get_class_type_hint_namespaces( + obj: type, +) -> tuple[dict[str, typing.Any], dict[str, typing.Any]]: + globalns: dict[str, typing.Any] = {} + localns: dict[str, typing.Any] = {} + + # Get module globals + if obj.__module__ and (module := sys.modules.get(obj.__module__)): + globalns.update(module.__dict__) + + # Annotations may use typevars defined in the class + localns.update(obj.__dict__) + + if _typing_inspect.is_generic_alias(obj): + # We need the origin's type vars + localns.update(obj.__origin__.__dict__) + + # Extract type parameters from the class + args = typing.get_args(obj) + origin = typing.get_origin(obj) + tps = getattr(obj, '__type_params__', ()) or getattr( + origin, '__parameters__', () + ) + for tp, arg in zip(tps, args, strict=False): + localns[tp.__name__] = arg + + # Add the class itself for self-references + localns[obj.__name__] = obj + + return globalns, localns + + @_eval_types_impl.register def _eval_type_type(obj: type, ctx: EvalContext): + # Ensure that any string annotations are resolved + if ( + hasattr(obj, '__annotations__') + and obj.__annotations__ + and any(isinstance(v, str) for v in obj.__annotations__.values()) + ): + # Ensure we don't recurse infinitely + ctx.seen[obj] = obj + + # Replace string annotations with resolved types + globalns, localns = _get_class_type_hint_namespaces(obj) + hints = { + k: _eval_types(v, ctx) + for k, v in typing.get_type_hints( + obj, globalns=globalns, localns=localns, include_extras=True + ).items() + } + obj.__annotations__.update(hints) + return obj From 981ca328e3004465586bd75564dc5990fa920864 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Mon, 26 Jan 2026 13:14:07 -0800 Subject: [PATCH 3/3] Re-add TODO comment. --- typemap/type_eval/_apply_generic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/typemap/type_eval/_apply_generic.py b/typemap/type_eval/_apply_generic.py index 65e739a..8b2acd3 100644 --- a/typemap/type_eval/_apply_generic.py +++ b/typemap/type_eval/_apply_generic.py @@ -243,6 +243,7 @@ def get_local_defns(boxed: Boxed) -> tuple[dict[str, Any], dict[str, Any]]: else: annos[k] = v elif af := getattr(boxed.cls, "__annotations__", None): + # TODO: substitute vars in this case _globals = {} if mod := sys.modules.get(boxed.cls.__module__): _globals.update(vars(mod))