From abc47aea98dc33387bf70cb74dfc1b8d0d58b8d2 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Thu, 12 Feb 2026 18:58:26 -0800 Subject: [PATCH 1/6] Move function type construction to get_local_defns. --- typemap/type_eval/_apply_generic.py | 47 ++++++++++++++++++++++++++-- typemap/type_eval/_eval_operators.py | 40 ++++++----------------- 2 files changed, 53 insertions(+), 34 deletions(-) diff --git a/typemap/type_eval/_apply_generic.py b/typemap/type_eval/_apply_generic.py index 02f493a..6b499f9 100644 --- a/typemap/type_eval/_apply_generic.py +++ b/typemap/type_eval/_apply_generic.py @@ -15,6 +15,7 @@ if typing.TYPE_CHECKING: from typing import Any, Mapping + from typemap.typing import GenericCallable, Overloaded @dataclasses.dataclass(frozen=True) @@ -324,10 +325,18 @@ def get_local_defns( ) -> tuple[ dict[str, Any], dict[ - str, types.FunctionType | classmethod | staticmethod | WrappedOverloads + str, + type[ + typing.Callable + | classmethod + | staticmethod + | GenericCallable + | Overloaded + ], ], ]: - from typemap.typing import GenericCallable + from typemap.typing import GenericCallable, Overloaded + from ._eval_operators import _function_type annos: dict[str, Any] = {} dct: dict[str, Any] = {} @@ -339,6 +348,9 @@ def get_local_defns( if name in EXCLUDED_ATTRIBUTES: continue + if orig is typing._no_init_or_replace_init: # type: ignore[attr-defined] + continue + stuff = inspect.unwrap(orig) if isinstance(stuff, types.FunctionType): @@ -405,7 +417,36 @@ def lam(*vs): elif orig.__class__ is staticmethod: local_fn = staticmethod(local_fn) - dct[name] = local_fn + if isinstance( + local_fn, + ( + types.FunctionType, + types.MethodType, + staticmethod, + classmethod, + ), + ): + dct[name] = _function_type( + local_fn, receiver_type=boxed.alias_type() + ) + + elif isinstance(local_fn, WrappedOverloads): + overload_types: typing.Sequence[ + type[ + typing.Callable + | classmethod + | staticmethod + | GenericCallable + ] + ] = [ + _function_type( + _eval_typing.eval_typing(of), + receiver_type=boxed.alias_type(), + ) + for of in local_fn.functions + ] + + dct[name] = Overloaded[*overload_types] # type: ignore[valid-type] return annos, dct diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index bf513d6..5336ad8 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -41,7 +41,6 @@ Member, Members, NewProtocol, - Overloaded, Param, RaiseError, Slice, @@ -154,35 +153,12 @@ def get_annotated_method_hints(cls, *, ctx): _, dct = _apply_generic.get_local_defns(abox) for name, attr in dct.items(): - if isinstance( + hints[name] = ( attr, - ( - types.FunctionType, - types.MethodType, - staticmethod, - classmethod, - ), - ): - if attr is typing._no_init_or_replace_init: - continue - - hints[name] = ( - _function_type(attr, receiver_type=acls), - ("ClassVar",), - object, - acls, - ) - elif isinstance(attr, _apply_generic.WrappedOverloads): - overloads = [ - _function_type(_eval_types(of, ctx), receiver_type=acls) - for of in attr.functions - ] - hints[name] = ( - Overloaded[*overloads], - ("ClassVar",), - object, - acls, - ) + ("ClassVar",), + object, + acls, + ) return hints @@ -763,7 +739,9 @@ def _ann(x): return f -def _function_type(func, *, receiver_type): +def _function_type( + func, *, receiver_type +) -> type[typing.Callable | classmethod | staticmethod | GenericCallable]: root = inspect.unwrap(func) sig = inspect.signature(root) f = _function_type_from_sig(sig, func, receiver_type=receiver_type) @@ -772,7 +750,7 @@ def _function_type(func, *, receiver_type): # Must store a lambda that performs type variable substitution type_params = root.__type_params__ callable_lambda = _create_generic_callable_lambda(f, type_params) - f = GenericCallable[tuple[*type_params], callable_lambda] + f = GenericCallable[tuple[*type_params], callable_lambda] # type: ignore[misc,valid-type] return f From 17e6bc6ff2b1468986923c5c8e1206f0dfb7b227 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Thu, 12 Feb 2026 19:20:04 -0800 Subject: [PATCH 2/6] Pass _function_type_from_sig the type instead of the whole function. --- typemap/type_eval/_apply_generic.py | 2 +- typemap/type_eval/_eval_operators.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/typemap/type_eval/_apply_generic.py b/typemap/type_eval/_apply_generic.py index 6b499f9..9cb5a3c 100644 --- a/typemap/type_eval/_apply_generic.py +++ b/typemap/type_eval/_apply_generic.py @@ -399,7 +399,7 @@ def lam(*vs): ) sig = _resolved_function_signature(fn, args) return _function_type_from_sig( - sig, o, receiver_type=cls + sig, type(o), receiver_type=cls ) return lam diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 5336ad8..25f25d8 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -679,7 +679,7 @@ def _callable_type_to_method(name, typ, ctx): return head(func) -def _function_type_from_sig(sig, func, *, receiver_type): +def _function_type_from_sig(sig, func_type, *, receiver_type): empty = inspect.Parameter.empty def _ann(x): @@ -691,12 +691,12 @@ def _ann(x): for i, p in enumerate(sig.parameters.values()): ann = p.annotation # Special handling for first argument on methods. - if i == 0 and receiver_type and not isinstance(func, staticmethod): + if i == 0 and receiver_type and func_type is not staticmethod: if ann is empty: ann = receiver_type else: if ( - isinstance(func, classmethod) + func_type is classmethod and typing.get_origin(ann) is type and (receiver_args := typing.get_args(ann)) ): @@ -729,9 +729,9 @@ def _ann(x): # TODO: Is doing the tuple for staticmethod/classmethod legit? # Putting a list in makes it unhashable... f: typing.Any # type: ignore[annotation-unchecked] - if isinstance(func, staticmethod): + if func_type is staticmethod: f = staticmethod[tuple[*params], ret] - elif isinstance(func, classmethod): + elif func_type is classmethod: f = classmethod[specified_receiver, tuple[*params[1:]], ret] else: f = typing.Callable[params, ret] @@ -744,7 +744,7 @@ def _function_type( ) -> type[typing.Callable | classmethod | staticmethod | GenericCallable]: root = inspect.unwrap(func) sig = inspect.signature(root) - f = _function_type_from_sig(sig, func, receiver_type=receiver_type) + f = _function_type_from_sig(sig, type(func), receiver_type=receiver_type) if root.__type_params__: # Must store a lambda that performs type variable substitution From c699d0dfae248db09cd92e84a23e14eb1ab691dd Mon Sep 17 00:00:00 2001 From: dnwpark Date: Fri, 13 Feb 2026 09:04:57 -0800 Subject: [PATCH 3/6] Use resolved function signature instead of making a new function. --- typemap/type_eval/_apply_generic.py | 113 +++++++++++++--------------- 1 file changed, 53 insertions(+), 60 deletions(-) diff --git a/typemap/type_eval/_apply_generic.py b/typemap/type_eval/_apply_generic.py index 9cb5a3c..2483d3c 100644 --- a/typemap/type_eval/_apply_generic.py +++ b/typemap/type_eval/_apply_generic.py @@ -286,8 +286,10 @@ def get_annotations( return rr -def _resolved_function_signature(func, args): - """Get the signature of a function with type hints resolved to arg values""" +def _resolved_function_signature( + func, args, definition_cls: type | None = None +): + """Get the signature of a function with hints resolved to arg values.""" import typemap.typing as nt @@ -306,7 +308,7 @@ def _resolved_function_signature(func, args): finally: nt.special_form_evaluator.reset(token) - if hints := get_annotations(func, args): + if hints := get_annotations(func, args, cls=definition_cls): params = [] for name, param in sig.parameters.items(): annotation = hints.get(name, param.annotation) @@ -336,7 +338,7 @@ def get_local_defns( ], ]: from typemap.typing import GenericCallable, Overloaded - from ._eval_operators import _function_type + from ._eval_operators import _function_type, _function_type_from_sig annos: dict[str, Any] = {} dct: dict[str, Any] = {} @@ -354,8 +356,6 @@ def get_local_defns( stuff = inspect.unwrap(orig) if isinstance(stuff, types.FunctionType): - local_fn: Any = None - # TODO: This annos_ok thing is a hack because processing # __annotations__ on methods broke stuff and I didn't want # to chase it down yet. @@ -368,24 +368,28 @@ def get_local_defns( stuck = True rr = None + resolved_sig = None if rr is not None: - local_fn = make_func(orig, rr) + resolved_sig = _resolved_function_signature( + stuff, boxed.str_args, definition_cls=boxed.cls + ) elif not stuck and getattr(stuff, "__annotations__", None): # XXX: This is totally wrong; we still need to do # substitute in class vars - local_fn = stuff - elif overloads := typing.get_overloads(stuff): - local_fn = WrappedOverloads(tuple(overloads)) - - # If we got stuck, we build a GenericCallable that - # computes the type once it has been given type - # variables! - if stuck and stuff.__type_params__: + resolved_sig = _resolved_function_signature( + stuff, boxed.str_args, definition_cls=boxed.cls + ) + overloads = typing.get_overloads(stuff) + + # If the method has type params, we build a GenericCallable + # (in annos only) so that [Z] etc. are preserved in output. + if stuff.__type_params__: type_params = stuff.__type_params__ str_args = boxed.str_args - canonical_cls = boxed.canonical_cls + receiver_cls = boxed.alias_type() + definition_cls = boxed.canonical_cls - def _make_lambda(fn, o, sa, tp, cls): + def _make_lambda(fn, o, sa, tp, recv_cls, def_cls): from ._eval_operators import _function_type_from_sig def lam(*vs): @@ -397,9 +401,11 @@ def lam(*vs): strict=True, ) ) - sig = _resolved_function_signature(fn, args) + sig = _resolved_function_signature( + fn, args, definition_cls=def_cls + ) return _function_type_from_sig( - sig, type(o), receiver_type=cls + sig, type(o), receiver_type=recv_cls ) return lam @@ -407,55 +413,42 @@ def lam(*vs): gc = GenericCallable[ # type: ignore[valid-type,misc] tuple[*type_params], # type: ignore[valid-type] _make_lambda( - stuff, orig, str_args, type_params, canonical_cls + stuff, + orig, + str_args, + type_params, + receiver_cls, + definition_cls, ), ] - annos[name] = typing.ClassVar[gc] - elif local_fn is not None: - if orig.__class__ is classmethod: - local_fn = classmethod(local_fn) - elif orig.__class__ is staticmethod: - local_fn = staticmethod(local_fn) - - if isinstance( - local_fn, - ( - types.FunctionType, - types.MethodType, - staticmethod, - classmethod, - ), - ): - dct[name] = _function_type( - local_fn, receiver_type=boxed.alias_type() - ) - - elif isinstance(local_fn, WrappedOverloads): - overload_types: typing.Sequence[ - type[ - typing.Callable - | classmethod - | staticmethod - | GenericCallable - ] - ] = [ - _function_type( - _eval_typing.eval_typing(of), - receiver_type=boxed.alias_type(), - ) - for of in local_fn.functions + dct[name] = gc + elif resolved_sig is not None: + dct[name] = _function_type_from_sig( + resolved_sig, + type(orig), + receiver_type=boxed.alias_type(), + ) + elif overloads: + overload_types: typing.Sequence[ + type[ + typing.Callable + | classmethod + | staticmethod + | GenericCallable ] + ] = [ + _function_type( + _eval_typing.eval_typing(of), + receiver_type=boxed.alias_type(), + ) + for of in overloads + ] - dct[name] = Overloaded[*overload_types] # type: ignore[valid-type] + dct[name] = Overloaded[*overload_types] # type: ignore[valid-type] return annos, dct -@dataclasses.dataclass(frozen=True) -class WrappedOverloads: - functions: tuple[typing.Callable[..., Any], ...] - - def flatten_class_new_proto(cls: type) -> type: # This is a hacky version of flatten_class that works by using # NewProtocol on Members! From 610738803d16c81b4ada82c99b57f790e059b985 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Fri, 13 Feb 2026 09:59:54 -0800 Subject: [PATCH 4/6] Don't use get_annotations. --- typemap/type_eval/_apply_generic.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/typemap/type_eval/_apply_generic.py b/typemap/type_eval/_apply_generic.py index 2483d3c..1440611 100644 --- a/typemap/type_eval/_apply_generic.py +++ b/typemap/type_eval/_apply_generic.py @@ -318,8 +318,10 @@ def _resolved_function_signature( sig = sig.replace( parameters=params, return_annotation=return_annotation ) + return sig - return sig + else: + return None def get_local_defns( @@ -359,26 +361,15 @@ def get_local_defns( # TODO: This annos_ok thing is a hack because processing # __annotations__ on methods broke stuff and I didn't want # to chase it down yet. - stuck = False - try: - rr = get_annotations( - stuff, boxed.str_args, cls=boxed.cls, annos_ok=False - ) - except _eval_typing.StuckException: - stuck = True - rr = None - resolved_sig = None - if rr is not None: - resolved_sig = _resolved_function_signature( - stuff, boxed.str_args, definition_cls=boxed.cls - ) - elif not stuck and getattr(stuff, "__annotations__", None): - # XXX: This is totally wrong; we still need to do - # substitute in class vars + try: resolved_sig = _resolved_function_signature( - stuff, boxed.str_args, definition_cls=boxed.cls + stuff, + boxed.str_args, + definition_cls=boxed.cls, ) + except _eval_typing.StuckException: + pass overloads = typing.get_overloads(stuff) # If the method has type params, we build a GenericCallable From c328baf8f38244e3bef5e6d80fcaa3e4a8cb6c71 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Fri, 13 Feb 2026 10:03:35 -0800 Subject: [PATCH 5/6] Move functions to _apply_generic. --- typemap/type_eval/_apply_generic.py | 107 ++++++++++++++++++++++++++- typemap/type_eval/_eval_operators.py | 102 ------------------------- 2 files changed, 104 insertions(+), 105 deletions(-) diff --git a/typemap/type_eval/_apply_generic.py b/typemap/type_eval/_apply_generic.py index 1440611..ed8bf6f 100644 --- a/typemap/type_eval/_apply_generic.py +++ b/typemap/type_eval/_apply_generic.py @@ -340,7 +340,6 @@ def get_local_defns( ], ]: from typemap.typing import GenericCallable, Overloaded - from ._eval_operators import _function_type, _function_type_from_sig annos: dict[str, Any] = {} dct: dict[str, Any] = {} @@ -381,8 +380,6 @@ def get_local_defns( definition_cls = boxed.canonical_cls def _make_lambda(fn, o, sa, tp, recv_cls, def_cls): - from ._eval_operators import _function_type_from_sig - def lam(*vs): args = dict(sa) args.update( @@ -440,6 +437,110 @@ def lam(*vs): return annos, dct +def _function_type_from_sig(sig, func_type, *, receiver_type): + from typemap.typing import Param + + empty = inspect.Parameter.empty + + def _ann(x): + return typing.Any if x is empty else None if x is type(None) else x + + specified_receiver = receiver_type + + params = [] + for i, p in enumerate(sig.parameters.values()): + ann = p.annotation + # Special handling for first argument on methods. + if i == 0 and receiver_type and func_type is not staticmethod: + if ann is empty: + ann = receiver_type + else: + if ( + func_type is classmethod + and typing.get_origin(ann) is type + and (receiver_args := typing.get_args(ann)) + ): + # The annotation for cls in a classmethod should be type[C] + specified_receiver = receiver_args[0] + else: + specified_receiver = ann + + quals = [] + if p.kind == inspect.Parameter.VAR_POSITIONAL: + quals.append("*") + if p.kind == inspect.Parameter.VAR_KEYWORD: + quals.append("**") + if p.kind == inspect.Parameter.KEYWORD_ONLY: + quals.append("keyword") + if p.kind == inspect.Parameter.POSITIONAL_ONLY: + quals.append("positional") + if p.default is not empty: + quals.append("default") + params.append( + Param[ + typing.Literal[p.name], + _ann(ann), + typing.Literal[*quals] if quals else typing.Never, + ] + ) + + ret = _ann(sig.return_annotation) + + # TODO: Is doing the tuple for staticmethod/classmethod legit? + # Putting a list in makes it unhashable... + f: typing.Any # type: ignore[annotation-unchecked] + if func_type is staticmethod: + f = staticmethod[tuple[*params], ret] + elif func_type is classmethod: + f = classmethod[specified_receiver, tuple[*params[1:]], ret] + else: + f = typing.Callable[params, ret] + + return f + + +def _function_type( + func, *, receiver_type +) -> type[typing.Callable | classmethod | staticmethod | GenericCallable]: + from typemap.typing import GenericCallable + + root = inspect.unwrap(func) + sig = inspect.signature(root) + f = _function_type_from_sig(sig, type(func), receiver_type=receiver_type) + + if root.__type_params__: + # Must store a lambda that performs type variable substitution + type_params = root.__type_params__ + callable_lambda = _create_generic_callable_lambda(f, type_params) + f = GenericCallable[tuple[*type_params], callable_lambda] # type: ignore[misc,valid-type] + return f + + +def _create_generic_callable_lambda( + f: typing.Callable | classmethod | staticmethod, + type_params: tuple[typing.TypeVar, ...], +): + if typing.get_origin(f) in (staticmethod, classmethod): + return lambda *vs: substitute( + f, dict(zip(type_params, vs, strict=True)) + ) + + else: + # Callable params are stored as a list + params, ret = typing.get_args(f) + + return lambda *vs: typing.Callable[ + [ + substitute( + p, + dict(zip(type_params, vs, strict=True)), + ) + for p in params + ], + substitute(ret, dict(zip(type_params, vs, strict=True))), + ] + + def flatten_class_new_proto(cls: type) -> type: # This is a hacky version of flatten_class that works by using # NewProtocol on Members! diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 25f25d8..7b45a50 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -679,108 +679,6 @@ def _callable_type_to_method(name, typ, ctx): return head(func) -def _function_type_from_sig(sig, func_type, *, receiver_type): - empty = inspect.Parameter.empty - - def _ann(x): - return typing.Any if x is empty else None if x is type(None) else x - - specified_receiver = receiver_type - - params = [] - for i, p in enumerate(sig.parameters.values()): - ann = p.annotation - # Special handling for first argument on methods. - if i == 0 and receiver_type and func_type is not staticmethod: - if ann is empty: - ann = receiver_type - else: - if ( - func_type is classmethod - and typing.get_origin(ann) is type - and (receiver_args := typing.get_args(ann)) - ): - # The annotation for cls in a classmethod should be type[C] - specified_receiver = receiver_args[0] - else: - specified_receiver = ann - - quals = [] - if p.kind == inspect.Parameter.VAR_POSITIONAL: - quals.append("*") - if p.kind == inspect.Parameter.VAR_KEYWORD: - quals.append("**") - if p.kind == inspect.Parameter.KEYWORD_ONLY: - quals.append("keyword") - if p.kind == inspect.Parameter.POSITIONAL_ONLY: - quals.append("positional") - if p.default is not empty: - quals.append("default") - params.append( - Param[ - typing.Literal[p.name], - _ann(ann), - typing.Literal[*quals] if quals else typing.Never, - ] - ) - - ret = _ann(sig.return_annotation) - - # TODO: Is doing the tuple for staticmethod/classmethod legit? - # Putting a list in makes it unhashable... - f: typing.Any # type: ignore[annotation-unchecked] - if func_type is staticmethod: - f = staticmethod[tuple[*params], ret] - elif func_type is classmethod: - f = classmethod[specified_receiver, tuple[*params[1:]], ret] - else: - f = typing.Callable[params, ret] - - return f - - -def _function_type( - func, *, receiver_type -) -> type[typing.Callable | classmethod | staticmethod | GenericCallable]: - root = inspect.unwrap(func) - sig = inspect.signature(root) - f = _function_type_from_sig(sig, type(func), receiver_type=receiver_type) - - if root.__type_params__: - # Must store a lambda that performs type variable substitution - type_params = root.__type_params__ - callable_lambda = _create_generic_callable_lambda(f, type_params) - f = GenericCallable[tuple[*type_params], callable_lambda] # type: ignore[misc,valid-type] - return f - - -def _create_generic_callable_lambda( - f: typing.Callable | classmethod | staticmethod, - type_params: tuple[typing.TypeVar, ...], -): - if typing.get_origin(f) in (staticmethod, classmethod): - return lambda *vs: _apply_generic.substitute( - f, dict(zip(type_params, vs, strict=True)) - ) - - else: - # Callable params are stored as a list - params, ret = typing.get_args(f) - - return lambda *vs: typing.Callable[ - [ - _apply_generic.substitute( - p, - dict(zip(type_params, vs, strict=True)), - ) - for p in params - ], - _apply_generic.substitute( - ret, dict(zip(type_params, vs, strict=True)) - ), - ] - - def _hint_to_member(n, t, qs, init, d, *, ctx): return Member[ typing.Literal[n], From 2bd7350ef760ba1aea8c059960611441459a3557 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Fri, 13 Feb 2026 10:31:10 -0800 Subject: [PATCH 6/6] Reorder the branches for clarity. --- typemap/type_eval/_apply_generic.py | 47 ++++++++++++++++------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/typemap/type_eval/_apply_generic.py b/typemap/type_eval/_apply_generic.py index ed8bf6f..44a033e 100644 --- a/typemap/type_eval/_apply_generic.py +++ b/typemap/type_eval/_apply_generic.py @@ -357,20 +357,6 @@ def get_local_defns( stuff = inspect.unwrap(orig) if isinstance(stuff, types.FunctionType): - # TODO: This annos_ok thing is a hack because processing - # __annotations__ on methods broke stuff and I didn't want - # to chase it down yet. - resolved_sig = None - try: - resolved_sig = _resolved_function_signature( - stuff, - boxed.str_args, - definition_cls=boxed.cls, - ) - except _eval_typing.StuckException: - pass - overloads = typing.get_overloads(stuff) - # If the method has type params, we build a GenericCallable # (in annos only) so that [Z] etc. are preserved in output. if stuff.__type_params__: @@ -410,13 +396,9 @@ def lam(*vs): ), ] dct[name] = gc - elif resolved_sig is not None: - dct[name] = _function_type_from_sig( - resolved_sig, - type(orig), - receiver_type=boxed.alias_type(), - ) - elif overloads: + + elif overloads := typing.get_overloads(stuff): + # If the method is overloaded, build an Overloaded type. overload_types: typing.Sequence[ type[ typing.Callable @@ -433,6 +415,29 @@ def lam(*vs): ] dct[name] = Overloaded[*overload_types] # type: ignore[valid-type] + continue + + else: + # Try to resolve the signature as a normal function. + resolved_sig = None + try: + resolved_sig = _resolved_function_signature( + stuff, + boxed.str_args, + definition_cls=boxed.cls, + ) + except _eval_typing.StuckException: + # We can get stuck if the signature has external type vars. + # Just fallback to the original signature for now. + resolved_sig = inspect.signature(stuff) + + if resolved_sig is not None: + dct[name] = _function_type_from_sig( + resolved_sig, + type(orig), + receiver_type=boxed.alias_type(), + ) + continue return annos, dct