diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 0ef7ff9..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]: ... """) @@ -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/tests/test_type_eval.py b/tests/test_type_eval.py index 5d954a8..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 @@ -480,11 +533,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 +551,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 +699,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 +713,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 +727,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 +1005,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/_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 0a89de6..20a218e 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,10 @@ 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) head = typing.get_origin(typ) if head is classmethod: @@ -585,10 +587,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. @@ -895,7 +927,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 +945,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] @@ -1082,7 +1125,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) diff --git a/typemap/typing.py b/typemap/typing.py index f765487..cfe64cb 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -5,7 +5,11 @@ import types from typing import Literal, Unpack -from typing import _GenericAlias, _LiteralGenericAlias, _UnpackGenericAlias +from typing import ( + _GenericAlias, + _LiteralGenericAlias, + _UnpackGenericAlias, +) _SpecialForm: typing.Any = typing._SpecialForm @@ -67,16 +71,26 @@ 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(_GenericAlias, _root=True): pass +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)) + + ###