From 8f59ebd982d4a1a198e56bf0daf04ce3475e63e4 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Mon, 9 Feb 2026 15:47:28 -0800 Subject: [PATCH 1/7] Expand existing tests. --- tests/test_type_eval.py | 120 +++++++++++++++++++++++++++++++--------- 1 file changed, 94 insertions(+), 26 deletions(-) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 09c407c..10faeae 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -15,13 +15,12 @@ Tuple, TypeVar, Union, - get_args, overload, ) import pytest -from typemap.type_eval import eval_typing +from typemap.type_eval import _ensure_context, eval_typing from typemap.typing import _BoolLiteral from typemap_extensions import ( @@ -428,20 +427,26 @@ def f[TX](self, x: TX) -> OnlyIntToSet[TX]: ... m = eval_typing(GetMember[C, Literal["f"]]) assert eval_typing(GetName[m]) == Literal["f"] + assert eval_typing(IsAssignable[GetType[m], GenericCallable]) 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]], - ] - ) + ft = m.__args__[1].__args__[1] + with _ensure_context(): + assert ( + ft(str) + == Callable[ + [Param[Literal["self"], C], Param[Literal["x"], str]], + str, + ] + ) + assert ( + ft(int) + == Callable[ + [Param[Literal["self"], C], Param[Literal["x"], int]], + set[int], + ] + ) def test_getmember_03(): @@ -452,20 +457,26 @@ def f[T](self, x: T) -> OnlyIntToSet[T]: ... m = eval_typing(GetMember[P, Literal["f"]]) assert eval_typing(GetName[m]) == Literal["f"] + assert eval_typing(IsAssignable[GetType[m], GenericCallable]) 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]], - ] - ) + ft = m.__args__[1].__args__[1] + with _ensure_context(): + assert ( + ft(str) + == Callable[ + [Param[Literal["self"], Self], Param[Literal["x"], str]], + OnlyIntToSet[str], + ] + ) + assert ( + ft(int) + == Callable[ + [Param[Literal["self"], Self], Param[Literal["x"], int]], + OnlyIntToSet[int], + ] + ) def test_getmember_04(): @@ -589,15 +600,18 @@ def test_eval_getarg_callable_02(): T = TypeVar("T") # Params not wrapped - f = Callable[[T], T] + f = lambda T: Callable[[T], T] gc = GenericCallable[tuple[T], f] t = eval_typing(GetArg[gc, GenericCallable, Literal[0]]) assert t == tuple[T] gc_f = eval_typing(GetArg[gc, GenericCallable, Literal[1]]) assert gc_f == Never + with _ensure_context(): + assert gc.__args__[1](str) == Callable[[str], str] + # Params wrapped - f = Callable[ + f = lambda T: Callable[ [ Param[Literal[None], T, Literal["positional"]], Param[Literal["y"], T], @@ -614,6 +628,19 @@ def test_eval_getarg_callable_02(): gc_f = eval_typing(GetArg[gc, GenericCallable, Literal[1]]) assert gc_f == Never + with _ensure_context(): + assert ( + gc.__args__[1](str) + == Callable[ + [ + Param[Literal[None], str, Literal["positional"]], + Param[Literal["y"], str], + Param[Literal["z"], str, Literal["keyword"]], + ], + str, + ] + ) + type IndirectProtocol[T] = NewProtocol[*[m for m in Iter[Members[T]]],] type GetMethodLike[T, Name] = GetArg[ @@ -762,6 +789,20 @@ def f[T](self, x: T, /, y: T, *, z: T) -> T: ... f = eval_typing(GetArg[gc, GenericCallable, Literal[1]]) assert f is Never + with _ensure_context(): + assert ( + gc.__args__[1](str) + == Callable[ + [ + Param[Literal["self"], C, Literal["positional"]], + Param[Literal["x"], str, Literal["positional"]], + Param[Literal["y"], str], + Param[Literal["z"], str, Literal["keyword"]], + ], + str, + ] + ) + def test_eval_getarg_callable_08(): # generic classmethod @@ -776,6 +817,20 @@ def f[T](cls, x: T, /, y: T, *, z: T) -> T: ... f = eval_typing(GetArg[gc, GenericCallable, Literal[1]]) assert f is Never + with _ensure_context(): + assert ( + gc.__args__[1](str) + == classmethod[ + C, + tuple[ + Param[Literal["x"], str, Literal["positional"]], + Param[Literal["y"], str], + Param[Literal["z"], str, Literal["keyword"]], + ], + str, + ] + ) + def test_eval_getarg_callable_09(): # generic staticmethod @@ -790,6 +845,19 @@ def f[T](x: T, /, y: T, *, z: T) -> T: ... f = eval_typing(GetArg[gc, GenericCallable, Literal[1]]) assert f is Never + with _ensure_context(): + assert ( + gc.__args__[1](str) + == staticmethod[ + tuple[ + Param[Literal["x"], str, Literal["positional"]], + Param[Literal["y"], str], + Param[Literal["z"], str, Literal["keyword"]], + ], + str, + ] + ) + def test_eval_getarg_tuple(): t = tuple[int, ...] From a91ae7c0d5fc345f3ddb762b1f5ce42786ce988d Mon Sep 17 00:00:00 2001 From: dnwpark Date: Mon, 9 Feb 2026 15:35:42 -0800 Subject: [PATCH 2/7] Add tests. --- tests/test_type_eval.py | 216 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 214 insertions(+), 2 deletions(-) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 10faeae..9dbf47f 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -434,14 +434,14 @@ def f[TX](self, x: TX) -> OnlyIntToSet[TX]: ... ft = m.__args__[1].__args__[1] with _ensure_context(): assert ( - ft(str) + eval_typing(ft(str)) == Callable[ [Param[Literal["self"], C], Param[Literal["x"], str]], str, ] ) assert ( - ft(int) + eval_typing(ft(int)) == Callable[ [Param[Literal["self"], C], Param[Literal["x"], int]], set[int], @@ -519,6 +519,218 @@ def f(self, x): ... ) +def test_getmember_05(): + class C: + def member_method(self, x: T) -> T: ... + @classmethod + def class_method(cls, x: T) -> T: ... + @staticmethod + def static_method(x: T) -> T: ... + + # member method + m = eval_typing(GetMember[C, Literal["member_method"]]) + assert eval_typing(GetName[m]) == Literal["member_method"] + assert eval_typing(IsAssignable[GetType[m], GenericCallable]) + assert eval_typing(GetQuals[m]) == Literal["ClassVar"] + assert eval_typing(GetDefiner[m]) == C + + ft = m.__args__[1].__args__[1] + with _ensure_context(): + assert ( + ft(str) + == Callable[ + [Param[Literal["self"], C], Param[Literal["x"], str, Never]], + str, + ] + ) + + # class method + m = eval_typing(GetMember[C, Literal["class_method"]]) + assert eval_typing(GetName[m]) == Literal["class_method"] + assert eval_typing(IsAssignable[GetType[m], GenericCallable]) + assert eval_typing(GetQuals[m]) == Literal["ClassVar"] + assert eval_typing(GetDefiner[m]) == C + + ft = m.__args__[1].__args__[1] + with _ensure_context(): + assert ( + ft(str) + == classmethod[ + C, + tuple[Param[Literal["x"], str, Never]], + str, + ] + ) + + # static method + m = eval_typing(GetMember[C, Literal["static_method"]]) + assert eval_typing(GetName[m]) == Literal["static_method"] + assert eval_typing(IsAssignable[GetType[m], GenericCallable]) + assert eval_typing(GetQuals[m]) == Literal["ClassVar"] + assert eval_typing(GetDefiner[m]) == C + + ft = m.__args__[1].__args__[1] + with _ensure_context(): + assert ( + ft(str) == staticmethod[tuple[Param[Literal["x"], str, Never]], str] + ) + + +def test_getmember_06(): + class C[T]: + def member_method( + self, x: T + ) -> set[T] if IsAssignable[T, int] else T: ... + @classmethod + def class_method( + cls, x: T + ) -> set[T] if IsAssignable[T, int] else T: ... + @staticmethod + def static_method(x: T) -> set[T] if IsAssignable[T, int] else T: ... + + # member method + m = eval_typing(GetMember[C[int], Literal["member_method"]]) + assert eval_typing(GetName[m]) == Literal["member_method"] + assert ( + eval_typing(GetType[m]) + == Callable[ + [Param[Literal["self"], C[int]], Param[Literal["x"], int]], set[int] + ] + ) + assert eval_typing(GetQuals[m]) == Literal["ClassVar"] + assert eval_typing(GetDefiner[m]) == C[int] + + m = eval_typing(GetMember[C[str], Literal["member_method"]]) + assert eval_typing(GetName[m]) == Literal["member_method"] + assert ( + eval_typing(GetType[m]) + == Callable[ + [Param[Literal["self"], C[str]], Param[Literal["x"], str]], str + ] + ) + assert eval_typing(GetQuals[m]) == Literal["ClassVar"] + assert eval_typing(GetDefiner[m]) == C[str] + + # class method + m = eval_typing(GetMember[C[int], Literal["class_method"]]) + assert eval_typing(GetName[m]) == Literal["class_method"] + assert ( + eval_typing(GetType[m]) + == classmethod[C[int], tuple[Param[Literal["x"], int]], set[int]] + ) + assert eval_typing(GetQuals[m]) == Literal["ClassVar"] + assert eval_typing(GetDefiner[m]) == C[int] + + m = eval_typing(GetMember[C[str], Literal["class_method"]]) + assert eval_typing(GetName[m]) == Literal["class_method"] + assert ( + eval_typing(GetType[m]) + == classmethod[C[str], tuple[Param[Literal["x"], str]], str] + ) + assert eval_typing(GetQuals[m]) == Literal["ClassVar"] + assert eval_typing(GetDefiner[m]) == C[str] + + # static method + m = eval_typing(GetMember[C[int], Literal["static_method"]]) + assert eval_typing(GetName[m]) == Literal["static_method"] + assert ( + eval_typing(GetType[m]) + == staticmethod[tuple[Param[Literal["x"], int]], set[int]] + ) + assert eval_typing(GetQuals[m]) == Literal["ClassVar"] + assert eval_typing(GetDefiner[m]) == C[int] + + m = eval_typing(GetMember[C[str], Literal["static_method"]]) + assert eval_typing(GetName[m]) == Literal["static_method"] + assert ( + eval_typing(GetType[m]) + == staticmethod[tuple[Param[Literal["x"], str]], str] + ) + assert eval_typing(GetQuals[m]) == Literal["ClassVar"] + assert eval_typing(GetDefiner[m]) == C[str] + + +def test_getmember_07(): + # T defined externally + class C: + def member_method( + self, x: T + ) -> set[T] if IsAssignable[T, int] else T: ... + @classmethod + def class_method( + cls, x: T + ) -> set[T] if IsAssignable[T, int] else T: ... + @staticmethod + def static_method(x: T) -> set[T] if IsAssignable[T, int] else T: ... + + # member method + m = eval_typing(GetMember[C, Literal["member_method"]]) + assert eval_typing(GetName[m]) == Literal["member_method"] + assert eval_typing(IsAssignable[GetType[m], GenericCallable]) + assert eval_typing(GetQuals[m]) == Literal["ClassVar"] + assert eval_typing(GetDefiner[m]) == C + + ft = m.__args__[1].__args__[1] + with _ensure_context(): + assert ( + ft(str) + == Callable[ + [Param[Literal["self"], C], Param[Literal["x"], str, Never]], + str, + ] + ) + assert ( + ft(int) + == Callable[ + [Param[Literal["self"], C], Param[Literal["x"], int, Never]], + set[int], + ] + ) + + # class method + m = eval_typing(GetMember[C, Literal["class_method"]]) + assert eval_typing(GetName[m]) == Literal["class_method"] + assert eval_typing(IsAssignable[GetType[m], GenericCallable]) + assert eval_typing(GetQuals[m]) == Literal["ClassVar"] + assert eval_typing(GetDefiner[m]) == C + + ft = m.__args__[1].__args__[1] + with _ensure_context(): + assert ( + ft(str) + == classmethod[ + C, + tuple[Param[Literal["x"], str, Never]], + str, + ] + ) + assert ( + ft(int) + == classmethod[ + C, + tuple[Param[Literal["x"], int, Never]], + set[int], + ] + ) + + # static method + m = eval_typing(GetMember[C, Literal["static_method"]]) + assert eval_typing(GetName[m]) == Literal["static_method"] + assert eval_typing(IsAssignable[GetType[m], GenericCallable]) + assert eval_typing(GetQuals[m]) == Literal["ClassVar"] + assert eval_typing(GetDefiner[m]) == C + + ft = m.__args__[1].__args__[1] + with _ensure_context(): + assert ( + ft(str) == staticmethod[tuple[Param[Literal["x"], str, Never]], str] + ) + assert ( + ft(int) + == staticmethod[tuple[Param[Literal["x"], int, Never]], set[int]] + ) + + def test_getarg_never(): d = eval_typing(GetArg[Never, object, Literal[0]]) assert d is Never From f192a99e5fed6f93961d514bf45bf1a875a6cf49 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Mon, 9 Feb 2026 15:36:10 -0800 Subject: [PATCH 3/7] Extract type vars that are from outside the function. --- typemap/type_eval/_apply_generic.py | 115 +++++++++++++++++++++------ typemap/type_eval/_eval_operators.py | 13 ++- 2 files changed, 102 insertions(+), 26 deletions(-) diff --git a/typemap/type_eval/_apply_generic.py b/typemap/type_eval/_apply_generic.py index e40c451..25440a4 100644 --- a/typemap/type_eval/_apply_generic.py +++ b/typemap/type_eval/_apply_generic.py @@ -13,7 +13,7 @@ if typing.TYPE_CHECKING: - from typing import Any, Mapping + from typing import Any, Mapping, MutableMapping @dataclasses.dataclass(frozen=True) @@ -206,13 +206,25 @@ def make_func( def get_annotations( obj: object, - args: Mapping[str, object], + args: MutableMapping[str, object], key: str = '__annotate__', cls: type | None = None, annos_ok: bool = True, ) -> Any | None: """Get the annotations on an object, substituting in type vars.""" + rr, globs = _get_raw_annotations(obj, args, key, annos_ok) + args = _args_with_type_params(obj, args, cls) + rr = _eval_raw_annotations(args, rr, globs) + return rr + + +def _get_raw_annotations( + obj: object, + args: Mapping[str, object], + key: str = '__annotate__', + annos_ok: bool = True, +) -> tuple[Any | None, dict[str, Any] | None]: rr = None globs = None if af := typing.cast(types.FunctionType, getattr(obj, key, None)): @@ -237,24 +249,73 @@ def get_annotations( # modify the original. rr = dict(rr) - if isinstance(rr, dict) and any(isinstance(v, str) for v in rr.values()): + return rr, globs + + +def _type_param_name( + param: typing.TypeVar | typing.ParamSpec | typing.TypeVarTuple, +) -> str: + """Name used in annotations (e.g. 'T'); str(param) can be '~T'.""" + return getattr(param, "__name__", str(param)) + + +def _args_with_type_params( + obj: object, args: MutableMapping[str, object], cls: type | None +) -> MutableMapping[str, object]: + # Copy in any __type_params__ that aren't provided for, so that if + # we have to eval, we have them. + if params := getattr(obj, "__type_params__", None): args = dict(args) - # Copy in any __type_params__ that aren't provided for, so that if - # we have to eval, we have them. - if params := getattr(obj, "__type_params__", None): - for param in params: - if str(param) not in args: - args[str(param)] = param - - # Include the class itself in args so that self-referential string - # annotations (e.g. from `from __future__ import annotations`) in - # nested scopes can be resolved during eval. (This only half - # solves that general problem, but it is the best we can do.) - rcls = cls or obj - if isinstance(rcls, (type, typing.TypeAliasType)): - if rcls.__name__ not in args: - args[rcls.__name__] = rcls + for param in params: + name = _type_param_name(param) + if name not in args: + args[name] = param + + # Include the class itself in args so that self-referential string + # annotations (e.g. from `from __future__ import annotations`) in + # nested scopes can be resolved during eval. (This only half + # solves that general problem, but it is the best we can do.) + rcls = cls or obj + if isinstance(rcls, (type, typing.TypeAliasType)): + if rcls.__name__ not in args: + args[rcls.__name__] = rcls + return args + + +def _find_annotation_type_vars( + args: Mapping[str, object], + rr: Any | None, + globs: dict[str, Any] | None, +) -> list[typing.TypeVar]: + """Get the type vars used in a function's annotations.""" + type_vars = [] + if isinstance(rr, dict) and any(isinstance(v, str) for v in rr.values()): + # For now, only handle plain type vars. + # TODO: + # - pattern matched annotations: T | None, set[T], etc. + # - type vars in an expression: U if IsAssignable[T, int] else V + try: + for v in rr.values(): + v = eval(v, globs, args) + if isinstance(v, typing.TypeVar): + type_vars.append(v) + except _eval_typing.StuckException: + pass + + return type_vars + + +def _eval_raw_annotations( + args: Mapping[str, object], + rr: Any | None, + globs: dict[str, Any] | None, +) -> Any | None: + if ( + isinstance(rr, dict) + and any(isinstance(v, str) for v in rr.values()) + and isinstance(globs, dict) + ): for k, v in rr.items(): # Eval strings if isinstance(v, str): @@ -324,10 +385,19 @@ def get_local_defns( # __annotations__ on methods broke stuff and I didn't want # to chase it down yet. stuck = False + type_params = list(stuff.__type_params__) try: - rr = get_annotations( - stuff, boxed.str_args, cls=boxed.cls, annos_ok=False + rr, globs = _get_raw_annotations( + stuff, boxed.str_args, annos_ok=False + ) + raw_args = _args_with_type_params( + stuff, boxed.str_args, boxed.cls ) + + for tv in _find_annotation_type_vars(raw_args, rr, globs): + if tv not in type_params: + type_params.append(tv) + rr = _eval_raw_annotations(raw_args, rr, globs) except _eval_typing.StuckException: stuck = True rr = None @@ -344,8 +414,7 @@ def get_local_defns( # 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__: - type_params = stuff.__type_params__ + if stuck and type_params: str_args = boxed.str_args def _make_lambda(fn, o, sa, tp): @@ -355,7 +424,7 @@ def lam(*vs): args = dict(sa) args.update( zip( - (str(p) for p in tp), + (_type_param_name(p) for p in tp), vs, strict=True, ) diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index fce90e6..8f0d9db 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -218,7 +218,7 @@ def _get_update_class_members( return None init_subclass = inspect.unwrap(init_subclass) - args = {} + args: dict[str, object] = {} # Get any type params from the base class if it is generic if (base_args := boxed_base.args.values()) and ( origin_params := getattr(base_origin, '__type_params__', None) @@ -711,9 +711,16 @@ 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 not isinstance(func, staticmethod): if ann is empty: - ann = receiver_type + # Self if no receiver, else the class. + ann = ( + receiver_type + if receiver_type + else type[typing.Self] + if isinstance(func, classmethod) + else typing.Self + ) else: if ( isinstance(func, classmethod) From ea37123d61952b1524333438de05fa8f45a8d3e8 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Mon, 9 Feb 2026 16:28:49 -0800 Subject: [PATCH 4/7] Pass receiver type to generic lambda. --- typemap/type_eval/_apply_generic.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/typemap/type_eval/_apply_generic.py b/typemap/type_eval/_apply_generic.py index 25440a4..2f257ac 100644 --- a/typemap/type_eval/_apply_generic.py +++ b/typemap/type_eval/_apply_generic.py @@ -416,8 +416,9 @@ def get_local_defns( # variables! if stuck and type_params: str_args = boxed.str_args + canonical_cls = boxed.canonical_cls - def _make_lambda(fn, o, sa, tp): + def _make_lambda(fn, o, sa, tp, cls): from ._eval_operators import _function_type_from_sig def lam(*vs): @@ -431,14 +432,16 @@ def lam(*vs): ) sig = _resolved_function_signature(fn, args) return _function_type_from_sig( - sig, o, receiver_type=None + sig, o, receiver_type=cls ) return lam gc = GenericCallable[ # type: ignore[valid-type,misc] tuple[*type_params], # type: ignore[valid-type] - _make_lambda(stuff, orig, str_args, type_params), + _make_lambda( + stuff, orig, str_args, type_params, canonical_cls + ), ] annos[name] = typing.ClassVar[gc] elif local_fn is not None: From 50c1ad1e212743d71d9c53a5a75461330e0e4a21 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Mon, 9 Feb 2026 17:43:01 -0800 Subject: [PATCH 5/7] Find extra type vars in function types. --- typemap/type_eval/_apply_generic.py | 5 +++- typemap/type_eval/_eval_operators.py | 34 +++++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/typemap/type_eval/_apply_generic.py b/typemap/type_eval/_apply_generic.py index 2f257ac..3f0cdaf 100644 --- a/typemap/type_eval/_apply_generic.py +++ b/typemap/type_eval/_apply_generic.py @@ -288,7 +288,10 @@ def _find_annotation_type_vars( rr: Any | None, globs: dict[str, Any] | None, ) -> list[typing.TypeVar]: - """Get the type vars used in a function's annotations.""" + """Get the type vars used in a function's annotations. + + Mirrors _eval_operators._collect_type_vars. + """ type_vars = [] if isinstance(rr, dict) and any(isinstance(v, str) for v in rr.values()): # For now, only handle plain type vars. diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 8f0d9db..a7a8543 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -766,14 +766,42 @@ def _ann(x): return f +def _find_function_type_vars(ty, *, seen=None): + """Find type vars in a function tyoe. + + Mirrors _apply_generic._find_annotation_type_vars. + """ + type_vars = [] + if typing.get_origin(ty) is classmethod: + _, params, ret = typing.get_args(ty) + params = params.__args__ + elif typing.get_origin(ty) is staticmethod: + params, ret = typing.get_args(ty) + params = params.__args__ + else: + params, ret = typing.get_args(ty) + + for param in params: + _, type, _ = typing.get_args(param) + if _typing_inspect.is_type_var(type): + type_vars.append(type) + if _typing_inspect.is_type_var(ret): + type_vars.append(ret) + + return type_vars + + def _function_type(func, *, receiver_type): root = inspect.unwrap(func) sig = inspect.signature(root) f = _function_type_from_sig(sig, func, receiver_type=receiver_type) - if root.__type_params__: - # Must store a lambda that performs type variable substitution - type_params = root.__type_params__ + type_params = list(getattr(root, "__type_params__", ()) or ()) + for tv in _find_function_type_vars(f): + if tv not in type_params: + type_params.append(tv) + + if type_params: callable_lambda = _create_generic_callable_lambda(f, type_params) f = GenericCallable[tuple[*type_params], callable_lambda] return f From 233fbc3a78a4a1d249d2de2a10db6be6dc650c8f Mon Sep 17 00:00:00 2001 From: dnwpark Date: Mon, 9 Feb 2026 17:28:05 -0800 Subject: [PATCH 6/7] Fix existing tests. --- tests/test_type_dir.py | 47 ++++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 19bf2d1..469e83b 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -16,6 +16,7 @@ Member, Members, NewProtocol, + Param, Uppercase, ) @@ -205,11 +206,11 @@ class Final: x: tests.test_type_dir.Wrapper[int | None] ordinary: str def foo(self: Self, a: int | None, *, b: int = ...) -> dict[str, int]: ... - def base[Z](self: Self, a: int | Z | None, b: ~K) -> dict[str, int | Z]: ... + def base[Z, ~K](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]: ... + def cbase[~K](cls: type[typing.Self], a: int | None, b: ~K) -> dict[str, int]: ... @staticmethod - def sbase[Z](a: OrGotcha[int] | Z | None, b: ~K) -> dict[str, int | Z]: ... + def sbase[Z, ~K](a: OrGotcha[int] | Z | None, b: ~K) -> dict[str, int | Z]: ... """) @@ -409,11 +410,19 @@ def test_type_members_func_2(): assert name == typing.Literal["cbase"] assert quals == typing.Literal["ClassVar"] - assert ( - str(typ) - == "\ -classmethod[tests.test_type_dir.Base[int], tuple[typemap.typing.Param[typing.Literal['a'], int | None, typing.Never], typemap.typing.Param[typing.Literal['b'], ~K, typing.Never]], dict[str, int]]" - ) + assert str(typ) == "typemap.typing.GenericCallable[tuple[~K], <...>]" + + with _ensure_context(): + assert ( + typ.__args__[1](float) + == classmethod[ + Base[int], + tuple[ + Param[Literal["a"], int | None], Param[Literal["b"], float] + ], + dict[str, int], + ] + ) def test_type_members_func_3(): @@ -424,15 +433,21 @@ def test_type_members_func_3(): assert name == typing.Literal["sbase"] assert quals == typing.Literal["ClassVar"] - assert str(typ) == "typemap.typing.GenericCallable[tuple[Z], <...>]" + assert str(typ) == "typemap.typing.GenericCallable[tuple[Z, ~K], <...>]" - 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]]" - ) + with _ensure_context(): + assert ( + eval_typing(typ.__args__[1](float, bool)) + == staticmethod[ + tuple[ + Param[ + Literal["a"], int | Literal['gotcha!'] | float | None + ], + Param[Literal["b"], bool], + ], + dict[str, int | float], + ] + ) # Test initializers From fcfe2c0a298f1963b5401b1c696f1a94dd074719 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Mon, 9 Feb 2026 17:46:33 -0800 Subject: [PATCH 7/7] Use __name__ in format_helper. --- tests/format_helper.py | 2 +- tests/test_type_dir.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/format_helper.py b/tests/format_helper.py index 0b9fb5e..8a64641 100644 --- a/tests/format_helper.py +++ b/tests/format_helper.py @@ -11,7 +11,7 @@ def format_meth(name, meth): ts = "" if params := root.__type_params__: - ts = "[" + ", ".join(str(p) for p in params) + "]" + ts = "[" + ", ".join(p.__name__ for p in params) + "]" return f"{name}{ts}{sig}" diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 469e83b..49c73ee 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -206,11 +206,11 @@ class Final: x: tests.test_type_dir.Wrapper[int | None] ordinary: str def foo(self: Self, a: int | None, *, b: int = ...) -> dict[str, int]: ... - def base[Z, ~K](self: Self, a: int | Z | None, b: ~K) -> dict[str, int | Z]: ... + def base[Z, K](self: Self, a: int | Z | None, b: ~K) -> dict[str, int | Z]: ... @classmethod - def cbase[~K](cls: type[typing.Self], a: int | None, b: ~K) -> dict[str, int]: ... + def cbase[K](cls: type[typing.Self], a: int | None, b: ~K) -> dict[str, int]: ... @staticmethod - def sbase[Z, ~K](a: OrGotcha[int] | Z | None, b: ~K) -> dict[str, int | Z]: ... + def sbase[Z, K](a: OrGotcha[int] | Z | None, b: ~K) -> dict[str, int | Z]: ... """)