diff --git a/spec-draft.rst b/spec-draft.rst index 653408e..3505dde 100644 --- a/spec-draft.rst +++ b/spec-draft.rst @@ -29,8 +29,8 @@ We introduce a ``Param`` type the contains all the information about a function ParamQuals = typing.Literal["*", "**", "default", "keyword"] - type PosParam[T] = Param[Literal[None], T] - type PosDefaultParam[T] = Param[Literal[None], T, Literal["default"]] + type PosParam[N: str | None, T] = Param[N, T, Literal["positional"]] + type PosDefaultParam[N: str | None, T] = Param[N, T, Literal["positional", "default"]] type DefaultParam[N: str, T] = Param[N, T, Literal["default"]] type NamedParam[N: str, T] = Param[N, T, Literal["keyword"]] type NamedDefaultParam[N: str, T] = Param[N, T, Literal["keyword", "default"]] @@ -55,7 +55,7 @@ as (we are omiting the ``Literal`` in places):: Callable[ [ - Param[None, int], + Param["a", int, "positional"], Param["b", int], Param["c", int, "default"], Param[None, int, "*"], @@ -71,7 +71,7 @@ or, using the type abbreviations we provide:: Callable[ [ - PosParam[int], + PosParam["a", int], Param["b", int], DefaultParam["c", int, ArgsParam[int, "*"], diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index bf4e32d..71387db 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -218,7 +218,7 @@ def test_type_dir_1b(): assert format_helper.format_class(d) == textwrap.dedent("""\ class CMethod: @classmethod - def cbase2(_arg0: type[tests.test_type_dir.CMethod], _arg1: int, /, a: bool | None) -> int: ... + def cbase2(cls: type[tests.test_type_dir.CMethod], lol: int, /, a: bool | None) -> int: ... """) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 9f16d38..0140e27 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -9,6 +9,7 @@ List, Literal, Never, + Self, Tuple, TypeVar, Union, @@ -30,6 +31,7 @@ Iter, Length, Member, + Members, NewProtocol, Param, SpecialFormEllipsis, @@ -137,6 +139,38 @@ class F[bool]: type NestedTree = str | list[NestedTree] | list[IntTree] +def test_eval_types_4(): + d = eval_typing( + Callable[ + [ + Param[Literal["a"], int, Literal["positional"]], + Param[Literal["b"], int], + Param[Literal["c"], int, Literal["default"]], + Param[None, int, Literal["*"]], + Param[Literal["d"], int, Literal["keyword"]], + Param[Literal["e"], int, Literal["default", "keyword"]], + Param[None, int, Literal["**"]], + ], + int, + ] + ) + assert ( + d + == Callable[ + [ + Param[Literal["a"], int, Literal["positional"]], + Param[Literal["b"], int], + Param[Literal["c"], int, Literal["default"]], + Param[None, int, Literal["*"]], + Param[Literal["d"], int, Literal["keyword"]], + Param[Literal["e"], int, Literal["default", "keyword"]], + Param[None, int, Literal["**"]], + ], + int, + ] + ) + + class TA: x: int y: list[float] @@ -381,7 +415,7 @@ def test_eval_getarg_callable_old(): assert args == Any -def test_eval_getarg_callable(): +def test_eval_getarg_callable_01(): t = Callable[[int, str], str] args = eval_typing(GetArg[t, Callable, 0]) assert ( @@ -413,6 +447,153 @@ def test_eval_getarg_callable(): assert args == Any +type IndirectProtocol[T] = NewProtocol[*[m for m in Iter[Members[T]]],] +type GetMethodLike[T, Name] = GetArg[ + tuple[ + *[ + GetType[p] + for p in Iter[Members[T]] + if ( + IsSub[GetType[p], Callable] + or IsSub[GetType[p], staticmethod] + or IsSub[GetType[p], classmethod] + ) + and IsSub[Name, GetName[p]] + ], + ], + tuple, + 0, +] + + +def test_eval_getarg_callable_02a(): + class C: + def f(self, x: int, /, y: int, *, z: int) -> int: ... + + f = eval_typing(GetMethodLike[C, Literal["f"]]) + t = eval_typing(GetArg[f, Callable, 0]) + assert ( + t + == tuple[ + Param[Literal["self"], C, Literal["positional"]], + Param[Literal["x"], int, Literal["positional"]], + Param[Literal["y"], int], + Param[Literal["z"], int, Literal["keyword"]], + ] + ) + t = eval_typing(GetArg[f, Callable, 1]) + assert t is int + + +def test_eval_getarg_callable_02b(): + class C: + def f(self, x: int, /, y: int, *, z: int) -> int: ... + + f = eval_typing(GetMethodLike[IndirectProtocol[C], Literal["f"]]) + t = eval_typing(GetArg[f, Callable, 0]) + assert ( + t + == tuple[ + Param[Literal["self"], Self, Literal["positional"]], + Param[Literal["x"], int, Literal["positional"]], + Param[Literal["y"], int], + Param[Literal["z"], int, Literal["keyword"]], + ] + ) + t = eval_typing(GetArg[f, Callable, 1]) + assert t is int + + +def test_eval_getarg_callable_03a(): + class C: + @classmethod + def f(cls, x: int, /, y: int, *, z: int) -> int: ... + + f = eval_typing(GetMethodLike[C, Literal["f"]]) + t = eval_typing(GetArg[f, classmethod, 0]) + assert t == C + t = eval_typing(GetArg[f, classmethod, 1]) + assert ( + t + == tuple[ + Param[Literal["x"], int, Literal["positional"]], + Param[Literal["y"], int], + Param[Literal["z"], int, Literal["keyword"]], + ] + ) + t = eval_typing(GetArg[f, classmethod, 2]) + assert t is int + + +def test_eval_getarg_callable_03b(): + class C: + @classmethod + def f(cls, x: int, /, y: int, *, z: int) -> int: ... + + f = eval_typing(GetMethodLike[IndirectProtocol[C], Literal["f"]]) + t = eval_typing(GetArg[f, Callable, 0]) + assert ( + t + == tuple[ + Param[Literal["cls"], type[C], Literal["positional"]], + Param[Literal["x"], int, Literal["positional"]], + Param[Literal["y"], int], + Param[Literal["z"], int, Literal["keyword"]], + ] + ) + t = eval_typing(GetArg[f, Callable, 1]) + assert t is int + + +def test_eval_getarg_callable_04a(): + class C: + @staticmethod + def f(x: int, /, y: int, *, z: int) -> int: ... + + f = eval_typing(GetMethodLike[C, Literal["f"]]) + t = eval_typing(GetArg[f, staticmethod, 0]) + assert ( + t + == tuple[ + Param[Literal["x"], int, Literal["positional"]], + Param[Literal["y"], int], + Param[Literal["z"], int, Literal["keyword"]], + ] + ) + t = eval_typing(GetArg[f, staticmethod, 1]) + assert t is int + + +def test_eval_getarg_callable_04b(): + class C: + @staticmethod + def f(x: int, /, y: int, *, z: int) -> int: ... + + f = eval_typing(GetMethodLike[IndirectProtocol[C], Literal["f"]]) + t = eval_typing(GetArg[f, Callable, 0]) + assert ( + t + == tuple[ + Param[Literal["x"], int, Literal["positional"]], + Param[Literal["y"], int], + Param[Literal["z"], int, Literal["keyword"]], + ] + ) + t = eval_typing(GetArg[f, Callable, 1]) + assert t is int + + +def test_eval_getarg_callable_05(): + class C: + f: Callable[[int], int] + + f = eval_typing(GetMethodLike[IndirectProtocol[C], Literal["f"]]) + t = eval_typing(GetArg[f, Callable, 0]) + assert t == tuple[Param[Literal[None], int, Never],] + t = eval_typing(GetArg[f, Callable, 1]) + assert t is int + + def test_eval_getarg_tuple(): t = tuple[int, ...] args = eval_typing(GetArg[t, tuple, 1]) diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 0694821..3489846 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -335,7 +335,7 @@ def _callable_type_to_signature(callable_type: object) -> inspect.Signature: elif "keyword" in quals: kind = inspect.Parameter.KEYWORD_ONLY saw_keyword_only = True - elif name is None: + elif "positional" in quals or name is None: kind = inspect.Parameter.POSITIONAL_ONLY elif saw_keyword_only: kind = inspect.Parameter.KEYWORD_ONLY @@ -395,8 +395,10 @@ def fn(*args, **kwargs): def _is_pos_only(param): name, _, quals = typing.get_args(param) - name = _from_literal(name) - return name is None and not (_get_quals(quals) & {"*", "**"}) + qual_set = _get_quals(quals) + return "positional" in qual_set or ( + name is None and not (_get_quals(quals) & {"*", "**"}) + ) def _callable_type_to_method(name, typ): @@ -419,16 +421,9 @@ def _callable_type_to_method(name, typ): cls, params, ret = typing.get_args(typ) # We have to make class positional only if there is some other # positional only argument. Annoying! - pname = ( - "cls" - if not any(_is_pos_only(p) for p in typing.get_args(params)) - else None - ) - cls_param = Param[ - typing.Literal[pname], - type[cls], - typing.Never, - ] + has_pos_only = any(_is_pos_only(p) for p in typing.get_args(params)) + quals = typing.Literal["positional"] if has_pos_only else typing.Never + cls_param = Param[typing.Literal["cls"], type[cls], quals] typ = typing.Callable[[cls_param] + list(typing.get_args(params)), ret] elif head is staticmethod: params, ret = typing.get_args(typ) @@ -485,10 +480,6 @@ def _ann(x): else: specified_receiver = ann - has_name = p.kind in ( - inspect.Parameter.POSITIONAL_OR_KEYWORD, - inspect.Parameter.KEYWORD_ONLY, - ) quals = [] if p.kind == inspect.Parameter.VAR_POSITIONAL: quals.append("*") @@ -496,11 +487,13 @@ def _ann(x): 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 if has_name else None], + typing.Literal[p.name], _ann(ann), typing.Literal[*quals] if quals else typing.Never, ] @@ -594,7 +587,11 @@ def _fix_callable_args(base, args): if typing.get_origin(special) is tuple: args[idx] = tuple[ *[ - t if isinstance(t, Param) else Param[typing.Literal[None], t] + ( + t + if typing.get_origin(t) is Param + else Param[typing.Literal[None], t] + ) for t in typing.get_args(special) ] ] diff --git a/typemap/typing.py b/typemap/typing.py index e7e7dd8..021483d 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -92,7 +92,7 @@ class Member[ definer: D -ParamQuals = Literal["*", "**", "keyword", "default"] +ParamQuals = Literal["*", "**", "keyword", "positional", "default"] class Param[N: str | None, T, Q: ParamQuals = typing.Never]: @@ -101,8 +101,10 @@ class Param[N: str | None, T, Q: ParamQuals = typing.Never]: quals: Q -type PosParam[T] = Param[Literal[None], T] -type PosDefaultParam[T] = Param[Literal[None], T, Literal["default"]] +type PosParam[N: str | None, T] = Param[N, T, Literal["positional"]] +type PosDefaultParam[N: str | None, T] = Param[ + N, T, Literal["positional", "default"] +] type DefaultParam[N: str, T] = Param[N, T, Literal["default"]] type NamedParam[N: str, T] = Param[N, T, Literal["keyword"]] type NamedDefaultParam[N: str, T] = Param[N, T, Literal["keyword", "default"]]