From 9315c3124741d30158b815c1e75c9c6a9e56a39c Mon Sep 17 00:00:00 2001 From: dnwpark Date: Tue, 3 Feb 2026 18:55:35 -0800 Subject: [PATCH 1/5] Change to special form. --- typemap/type_eval/_eval_operators.py | 7 +++++++ typemap/typing.py | 20 ++++++++++++-------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 0a89de6..23daaba 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -46,6 +46,7 @@ Uncapitalize, Uppercase, _BoolLiteral, + _GenericCallableGenericAlias, ) ################################################################## @@ -213,6 +214,12 @@ def wrapper(*args, ctx): ################################################################## +@type_eval.register_evaluator(GenericCallable) +@_lift_evaluated +def _eval_GenericCallable(tvs, c, *, ctx): + return _GenericCallableGenericAlias(tvs, c) + + @type_eval.register_evaluator(Iter) @_lift_evaluated def _eval_Iter(tp, *, ctx): diff --git a/typemap/typing.py b/typemap/typing.py index f765487..36b9d99 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -5,7 +5,12 @@ import types from typing import Literal, Unpack -from typing import _GenericAlias, _LiteralGenericAlias, _UnpackGenericAlias +from typing import ( + _GenericAlias, + _SpecialGenericAlias, + _LiteralGenericAlias, + _UnpackGenericAlias, +) _SpecialForm: typing.Any = typing._SpecialForm @@ -67,16 +72,15 @@ class SpecialFormEllipsis: ### -# We really need to be able to represent generic function types but it -# is a problem for all kinds of reasons... -# Can we bang it into Callable?? -class GenericCallable[ - TVs: tuple[typing.TypeVar, ...], - C: typing.Callable | staticmethod | classmethod, -]: +class _GenericCallableGenericAlias(_SpecialGenericAlias, _root=True): pass +@_SpecialForm +def GenericCallable(self, tps): + return _GenericCallableGenericAlias(self, tps) + + ### From 1dc1e612a802d4793fda2a75ea2b7342dc5a3a84 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Tue, 3 Feb 2026 19:31:47 -0800 Subject: [PATCH 2/5] Make it a class. --- typemap/type_eval/_eval_operators.py | 7 ------- typemap/typing.py | 20 +++++++++++++++----- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 23daaba..0a89de6 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -46,7 +46,6 @@ Uncapitalize, Uppercase, _BoolLiteral, - _GenericCallableGenericAlias, ) ################################################################## @@ -214,12 +213,6 @@ def wrapper(*args, ctx): ################################################################## -@type_eval.register_evaluator(GenericCallable) -@_lift_evaluated -def _eval_GenericCallable(tvs, c, *, ctx): - return _GenericCallableGenericAlias(tvs, c) - - @type_eval.register_evaluator(Iter) @_lift_evaluated def _eval_Iter(tp, *, ctx): diff --git a/typemap/typing.py b/typemap/typing.py index 36b9d99..cfe64cb 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -7,7 +7,6 @@ from typing import Literal, Unpack from typing import ( _GenericAlias, - _SpecialGenericAlias, _LiteralGenericAlias, _UnpackGenericAlias, ) @@ -72,13 +71,24 @@ class SpecialFormEllipsis: ### -class _GenericCallableGenericAlias(_SpecialGenericAlias, _root=True): +class _GenericCallableGenericAlias(_GenericAlias, _root=True): pass -@_SpecialForm -def GenericCallable(self, tps): - return _GenericCallableGenericAlias(self, tps) +class GenericCallable: + def __class_getitem__(cls, params): + message = ( + "GenericCallable must be used as " + "GenericCallable[tuple[TypeVar, ...], lambda : callable]." + ) + if not isinstance(params, tuple) or len(params) != 2: + raise TypeError(message) + + typevars, func = params + if not callable(func): + raise TypeError(message) + + return _GenericCallableGenericAlias(cls, (typevars, func)) ### From 154d430b724e5f78d5d4ac667bdd31d8bd7b4802 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Wed, 4 Feb 2026 09:13:30 -0800 Subject: [PATCH 3/5] Disallow access to second param of GenericCallable. --- tests/test_type_eval.py | 56 +++++++--------------------- typemap/type_eval/_eval_operators.py | 13 ++++++- 2 files changed, 26 insertions(+), 43 deletions(-) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 5d954a8..ff444d4 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -480,11 +480,7 @@ def test_eval_getarg_callable_02(): t = eval_typing(GetArg[gc, GenericCallable, Literal[0]]) assert t == tuple[T] gc_f = eval_typing(GetArg[gc, GenericCallable, Literal[1]]) - assert gc_f == f - t = eval_typing(GetArg[gc_f, Callable, Literal[0]]) - assert t == tuple[Param[Literal[None], T, Never]] - t = eval_typing(GetArg[gc_f, Callable, Literal[1]]) - assert t is T + assert gc_f == Never # Params wrapped f = Callable[ @@ -502,7 +498,7 @@ def test_eval_getarg_callable_02(): t = eval_typing(GetArg[gc, GenericCallable, Literal[0]]) assert t == tuple[T] gc_f = eval_typing(GetArg[gc, GenericCallable, Literal[1]]) - assert gc_f == f + assert gc_f == Never type IndirectProtocol[T] = NewProtocol[*[m for m in Iter[Members[T]]],] @@ -650,18 +646,7 @@ def f[T](self, x: T, /, y: T, *, z: T) -> T: ... GetArg[GetArg[gc, GenericCallable, Literal[0]], tuple, Literal[0]] ) f = eval_typing(GetArg[gc, GenericCallable, Literal[1]]) - t = eval_typing(GetArg[f, Callable, Literal[0]]) - assert ( - t - == tuple[ - Param[Literal["self"], C, Literal["positional"]], - Param[Literal["x"], _T, Literal["positional"]], - Param[Literal["y"], _T], - Param[Literal["z"], _T, Literal["keyword"]], - ] - ) - t = eval_typing(GetArg[f, Callable, Literal[1]]) - assert t is _T + assert f is Never def test_eval_getarg_callable_08(): @@ -675,19 +660,7 @@ def f[T](cls, x: T, /, y: T, *, z: T) -> T: ... GetArg[GetArg[gc, GenericCallable, Literal[0]], tuple, Literal[0]] ) f = eval_typing(GetArg[gc, GenericCallable, Literal[1]]) - t = eval_typing(GetArg[f, classmethod, Literal[0]]) - assert t is C - t = eval_typing(GetArg[f, classmethod, Literal[1]]) - assert ( - t - == tuple[ - Param[Literal["x"], _T, Literal["positional"]], - Param[Literal["y"], _T], - Param[Literal["z"], _T, Literal["keyword"]], - ] - ) - t = eval_typing(GetArg[f, classmethod, Literal[2]]) - assert t is _T + assert f is Never def test_eval_getarg_callable_09(): @@ -701,17 +674,7 @@ def f[T](x: T, /, y: T, *, z: T) -> T: ... GetArg[GetArg[gc, GenericCallable, Literal[0]], tuple, Literal[0]] ) f = eval_typing(GetArg[gc, GenericCallable, Literal[1]]) - t = eval_typing(GetArg[f, staticmethod, Literal[0]]) - assert ( - t - == tuple[ - Param[Literal["x"], _T, Literal["positional"]], - Param[Literal["y"], _T], - Param[Literal["z"], _T, Literal["keyword"]], - ] - ) - t = eval_typing(GetArg[f, staticmethod, Literal[1]]) - assert t is _T + assert f is Never def test_eval_getarg_tuple(): @@ -989,6 +952,15 @@ class Container2[T]: ... assert eval_typing(GetArg[t, Container, Literal[1]]) == Never +def test_eval_getargs_generic_callable_01(): + T = TypeVar("T") + t = GenericCallable[ + tuple[T], lambda T: Callable[[Param[Literal["x"], T]], int] + ] + args = eval_typing(GetArgs[t, GenericCallable]) + assert args == tuple[tuple[T]] + + class OuterType: class InnerType: pass diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 0a89de6..f98c037 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -895,7 +895,13 @@ def _eval_GetArg(tp, base, idx, *, ctx) -> typing.Any: return typing.Never try: - return _fix_type(args[_eval_literal(idx, ctx)]) + idx_val = _eval_literal(idx, ctx) + + if base_head is GenericCallable and idx_val >= 1: + # Disallow access to callable lambda + return typing.Never + + return _fix_type(args[idx_val]) except IndexError: return typing.Never @@ -907,6 +913,11 @@ def _eval_GetArgs(tp, base, *, ctx) -> typing.Any: args = _get_args(tp, base_head, ctx) if args is None: return typing.Never + + if base_head is GenericCallable: + # Disallow access to callable lambda + return tuple[args[0]] # type: ignore[valid-type] + return tuple[*args] # type: ignore[valid-type] From 195c4062aa58dc5dc4988ed47ad0c0e38c3dfb28 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Wed, 4 Feb 2026 09:28:40 -0800 Subject: [PATCH 4/5] Use lambda. --- tests/test_type_dir.py | 13 ++++++--- typemap/type_eval/_apply_generic.py | 2 +- typemap/type_eval/_eval_call.py | 6 +++- typemap/type_eval/_eval_operators.py | 42 +++++++++++++++++++++++++--- 4 files changed, 53 insertions(+), 10 deletions(-) diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 0ef7ff9..03061ad 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -410,10 +410,15 @@ def test_type_members_func_3(): assert ( str(typ) - # == "\ - # staticmethod[tuple[typemap.typing.Param[typing.Literal['a'], int | typing.Literal['gotcha!'] | Z | None, typing.Never], typemap.typing.Param[typing.Literal['b'], ~K, typing.Never]], dict[str, int | Z]]" - == "\ -typemap.typing.GenericCallable[tuple[Z], staticmethod[tuple[typemap.typing.Param[typing.Literal['a'], int | typing.Literal['gotcha!'] | Z | None, typing.Never], typemap.typing.Param[typing.Literal['b'], ~K, typing.Never]], dict[str, int | Z]]]" + == "typemap.typing.GenericCallable[tuple[Z], typemap.type_eval._eval_operators._create_generic_callable_lambda..]" + ) + + evaled = eval_typing( + typing.get_args(typ)[1](*typing.get_args(typing.get_args(typ)[0])) + ) + assert ( + str(evaled) + == "staticmethod[tuple[typemap.typing.Param[typing.Literal['a'], int | typing.Literal['gotcha!'] | Z | None, typing.Never], typemap.typing.Param[typing.Literal['b'], ~K, typing.Never]], dict[str, int | Z]]" ) diff --git a/typemap/type_eval/_apply_generic.py b/typemap/type_eval/_apply_generic.py index 48a7c11..49b8224 100644 --- a/typemap/type_eval/_apply_generic.py +++ b/typemap/type_eval/_apply_generic.py @@ -170,7 +170,7 @@ def make_func( func.__globals__, "__call__", func.__defaults__, - (), + func.__closure__, func.__kwdefaults__, ) diff --git a/typemap/type_eval/_eval_call.py b/typemap/type_eval/_eval_call.py index 0da09d3..9908db8 100644 --- a/typemap/type_eval/_eval_call.py +++ b/typemap/type_eval/_eval_call.py @@ -162,7 +162,11 @@ def eval_call_with_types( _typing_inspect.is_generic_alias(resolved_callable) and resolved_callable.__origin__ is GenericCallable ): - _, resolved_callable = typing.get_args(resolved_callable) + typevars_tuple, callable_lambda = typing.get_args(resolved_callable) + type_vars = typing.get_args(typevars_tuple) + resolved_callable = callable_lambda(*type_vars) + # Evaluate the result to expand type aliases + resolved_callable = _eval_typing.eval_typing(resolved_callable) sig = _callable_type_to_signature(resolved_callable) bound = sig.bind(*arg_types, **kwarg_types) diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index f98c037..2e4893c 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -471,7 +471,7 @@ def _is_pos_only(param): ) -def _callable_type_to_method(name, typ): +def _callable_type_to_method(name, typ, ctx): """Turn a callable type into a method. I'm not totally sure if this is worth doing! The main accomplishment @@ -482,8 +482,12 @@ def _callable_type_to_method(name, typ): head = typing.get_origin(typ) if head is GenericCallable: - ttparams, typ = typing.get_args(typ) + # Call the lambda with type variables to substitute the type variables + ttparams, ttfunc = typing.get_args(typ) type_params = typing.get_args(ttparams) + typ = ttfunc(*type_params) + # Evaluate the result to expand type aliases + typ = _eval_types(typ, ctx) head = typing.get_origin(typ) if head is classmethod: @@ -585,10 +589,40 @@ def _ann(x): else: f = typing.Callable[params, ret] if root.__type_params__: - f = GenericCallable[tuple[*root.__type_params__], f] + # 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] 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 _resolved_function_signature(func, receiver_type=None): """Get the signature of a function with type hints resolved. @@ -1093,7 +1127,7 @@ def _eval_NewProtocol(*etyps: Member, ctx): if type_eval.issubsimilar( typing.Literal["ClassVar"], tquals ) and _is_method_like(typ): - dct[name] = _callable_type_to_method(name, typ) + dct[name] = _callable_type_to_method(name, typ, ctx) else: annos[name] = _add_quals(typ, tquals) _unpack_init(dct, name, init) From 9236c843de81854b04bff477dc3ae08feae0f0df Mon Sep 17 00:00:00 2001 From: dnwpark Date: Wed, 4 Feb 2026 09:54:02 -0800 Subject: [PATCH 5/5] Don't eval the result of callable lambda. --- tests/test_type_dir.py | 2 +- tests/test_type_eval.py | 53 ++++++++++++++++++++++++++++ typemap/type_eval/_eval_operators.py | 2 -- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 03061ad..be3f8c0 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -208,7 +208,7 @@ def base[Z](self: Self, a: int | Z | None, b: ~K) -> dict[str, int | Z]: ... @classmethod def cbase(cls: type[typing.Self], a: int | None, b: ~K) -> dict[str, int]: ... @staticmethod - def sbase[Z](a: int | Literal['gotcha!'] | Z | None, b: ~K) -> dict[str, int | Z]: ... + def sbase[Z](a: OrGotcha[int] | Z | None, b: ~K) -> dict[str, int | Z]: ... """) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index ff444d4..245ea87 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -13,6 +13,7 @@ Tuple, TypeVar, Union, + get_args, ) import pytest @@ -25,9 +26,11 @@ GenericCallable, GetArg, GetArgs, + GetDefiner, GetMember, GetMemberType, GetName, + GetQuals, GetSpecialAttr, GetType, GetAnnotations, @@ -394,6 +397,56 @@ def test_getmember_01(): assert d == Never +def test_getmember_02(): + type OnlyIntToSet[T] = set[T] if IsSub[T, int] else T + + class C: + def f[T](self, x: T) -> OnlyIntToSet[T]: ... + + m = eval_typing(GetMember[C, Literal["f"]]) + assert eval_typing(GetName[m]) == Literal["f"] + assert eval_typing(GetQuals[m]) == Literal["ClassVar"] + assert eval_typing(GetDefiner[m]) == C + + t = eval_typing(GetType[m]) + Vs = get_args(get_args(t)[0]) + L = get_args(t)[1] + f = L(*Vs) + assert ( + f + == Callable[ + [Param[Literal["self"], C], Param[Literal["x"], Vs[0]]], + OnlyIntToSet[Vs[0]], + ] + ) + + +def test_getmember_03(): + type OnlyIntToSet[T] = set[T] if IsSub[T, int] else T + + class C: + def f[T](self, x: T) -> OnlyIntToSet[T]: ... + + type P = IndirectProtocol[C] + + m = eval_typing(GetMember[P, Literal["f"]]) + assert eval_typing(GetName[m]) == Literal["f"] + assert eval_typing(GetQuals[m]) == Literal["ClassVar"] + assert eval_typing(GetDefiner[m]) != C # eval typing generates a new class + + t = eval_typing(GetType[m]) + Vs = get_args(get_args(t)[0]) + L = get_args(t)[1] + f = L(*Vs) + assert ( + f + == Callable[ + [Param[Literal["self"], Self], Param[Literal["x"], Vs[0]]], + OnlyIntToSet[Vs[0]], + ] + ) + + def test_getarg_never(): d = eval_typing(GetArg[Never, object, Literal[0]]) assert d is Never diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 2e4893c..20a218e 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -486,8 +486,6 @@ def _callable_type_to_method(name, typ, ctx): ttparams, ttfunc = typing.get_args(typ) type_params = typing.get_args(ttparams) typ = ttfunc(*type_params) - # Evaluate the result to expand type aliases - typ = _eval_types(typ, ctx) head = typing.get_origin(typ) if head is classmethod: