diff --git a/spec-draft.rst b/spec-draft.rst index b456b75..b14f9b7 100644 --- a/spec-draft.rst +++ b/spec-draft.rst @@ -153,12 +153,12 @@ Methods are returned as callables using the new ``Param`` based extended callabl TODO: What do we do about decorators in general, *at runtime*... This seems pretty cursed. We can probably sometimes evaluate them, if there are annotations at runtime. -We also have helpers for extracting those names; they are all definable in terms of ``GetArg``. -(These names are too long -- but we can't do ``Type``. I kind of want to do the *longer* ``MemberName``?) +We also have helpers for extracting those names; they are all definable in terms of ``GetArg``. (Some of them are shared with ``Param``, discussed below.) +(These names are too long -- but we can't do ``Type``.) -* ``GetName[T: Member]`` -* ``GetType[T: Member]`` -* ``GetQuals[T: Member]`` +* ``GetName[T: Member | Param]`` +* ``GetType[T: Member | Param]`` +* ``GetQuals[T: Member | Param]`` * ``GetDefiner[T: Member]`` * ``NewProtocolWithBases[Bases, Ps: tuple[Member]]`` - A variant that allows specifying bases too. (UNIMPLEMENTED) @@ -174,14 +174,10 @@ We also have helpers for extracting those names; they are all definable in terms Callable inspection and creation -------------------------------- -* TODO: Should ``GetArg`` on a callable automatically convert ``[int, str]`` or whatever into something using ``Param``? Or should a separate operator be needed? +``Callable`` types always have their arguments exposed in the extended Callable format discussed above. -* ``GetParamName[T: Param]`` -* ``GetParamType[T: Param]`` -* ``GetParamQuals[T: Param]`` +The names, type, and qualifiers share getter operations with ``Member``. -This is unsatisfying; maybe they all need to be just ``ParamName`` and also ``MemberName`` above. -We could also merge the getters for ``Param`` and ``Member``. ---- diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index dcb0168..132350b 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -29,6 +29,7 @@ Length, Member, NewProtocol, + Param, SpecialFormEllipsis, StrConcat, StrSlice, @@ -349,7 +350,8 @@ def test_eval_getargs(): assert args == tuple[Any, Any] -def test_eval_getarg_callable(): +@unittest.skip +def test_eval_getarg_callable_old(): # oh hmmmmmmm -- yeah maybe callable could be fully bespoke if we # disallowed putting Callable here...! t = Callable[[int, str], str] @@ -377,6 +379,38 @@ def test_eval_getarg_callable(): assert args == Any +def test_eval_getarg_callable(): + t = Callable[[int, str], str] + args = eval_typing(GetArg[t, Callable, 0]) + assert ( + args + == tuple[ + Param[Literal[None], int, Never], Param[Literal[None], str, Never] + ] + ) + + t = Callable[int, str] + args = eval_typing(GetArg[t, Callable, 0]) + assert args == tuple[Param[Literal[None], int, Never]] + + t = Callable[[], str] + args = eval_typing(GetArg[t, Callable, 0]) + assert args == tuple[()] + + # XXX: Is this what we want? Or should it be *args, **kwargs + t = Callable[..., str] + args = eval_typing(GetArg[t, Callable, 0]) + assert args == SpecialFormEllipsis + + t = Callable + args = eval_typing(GetArg[t, Callable, 0]) + assert args == SpecialFormEllipsis + + t = Callable + args = eval_typing(GetArg[t, Callable, 1]) + assert args == Any + + 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 de91ad0..20cb4e0 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -500,11 +500,33 @@ def _eval_FromUnion(tp, *, ctx): @type_eval.register_evaluator(GetAttr) @_lift_over_unions -def _eval_GetAttr(lhs, prop, *, ctx): - # TODO: the prop missing, etc! +def _eval_GetAttr(tp, prop, *, ctx): # XXX: extras? - name = _eval_literal(prop, ctx) - return typing.get_type_hints(lhs)[name] + name = _from_literal(prop) + hints = { + **get_annotated_type_hints(tp, include_extras=True), + **get_annotated_method_hints(tp), + } + if name in hints: + return hints[name][0] + else: + return typing.Never + + +def _fix_callable_args(base, args): + idx = FUNC_LIKES[base] + if idx >= len(args): + return args + args = list(args) + special = _fix_type(args[idx]) + if typing.get_origin(special) is tuple: + args[idx] = tuple[ + *[ + t if isinstance(t, Param) else Param[typing.Literal[None], t] + for t in typing.get_args(special) + ] + ] + return tuple(args) def _get_raw_args(tp, base_head, ctx) -> typing.Any: @@ -515,7 +537,11 @@ def _get_raw_args(tp, base_head, ctx) -> typing.Any: return None if tp_head is base_head: - return typing.get_args(evaled) + args = typing.get_args(evaled) + if _is_method_like(tp): + args = _fix_callable_args(base_head, args) + + return args # Scan the fully-annotated MRO to find the base box = _apply_generic.box(tp) @@ -538,10 +564,12 @@ def _get_args(tp, base, ctx) -> typing.Any: def _fix_type(tp): """Fix up a type getting returned from GetArg - In particular, this means turning a list into a tuple of the list - elements and turning ... into SpecialFormEllipsis. + In particular, this means turning a list into a tuple of the + Paramified list elements and turning ... into SpecialFormEllipsis. + """ if isinstance(tp, (tuple, list)): + # XXX: Can we always do this or is it a problem return tuple[*tp] elif tp is ...: return SpecialFormEllipsis @@ -708,13 +736,16 @@ def _add_quals(typ, quals): return typ +FUNC_LIKES = { + GenericCallable: 0, + collections.abc.Callable: 0, + staticmethod: 0, + classmethod: 1, +} + + def _is_method_like(typ): - return typing.get_origin(typ) in ( - GenericCallable, - collections.abc.Callable, - staticmethod, - classmethod, - ) + return typing.get_origin(typ) in FUNC_LIKES @type_eval.register_evaluator(NewProtocol) diff --git a/typemap/typing.py b/typemap/typing.py index e972ad4..ce2f5ec 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -34,25 +34,25 @@ class GenericCallable[ class Member[N: str, T, Q: MemberQuals = typing.Never, D = typing.Never]: - pass - - -type GetName[T: Member] = GetArg[T, Member, 0] # type: ignore[valid-type] -type GetType[T: Member] = GetArg[T, Member, 1] # type: ignore[valid-type] -type GetQuals[T: Member] = GetArg[T, Member, 2] # type: ignore[valid-type] -type GetDefiner[T: Member] = GetArg[T, Member, 3] # type: ignore[valid-type] + name: N + typ: T + quals: Q + definer: D ParamQuals = typing.Literal["*", "**", "="] class Param[N: str | None, T, Q: ParamQuals = typing.Never]: - pass + name: N + typ: T + quals: Q -type GetParamName[T: Param] = GetArg[T, Param, 0] # type: ignore[valid-type] -type GetParamType[T: Param] = GetArg[T, Param, 1] # type: ignore[valid-type] -type GetParamQuals[T: Param] = GetArg[T, Param, 2] # type: ignore[valid-type] +type GetName[T: Member | Param] = GetAttr[T, typing.Literal["name"]] +type GetType[T: Member | Param] = GetAttr[T, typing.Literal["typ"]] +type GetQuals[T: Member | Param] = GetAttr[T, typing.Literal["quals"]] +type GetDefiner[T: Member] = GetAttr[T, typing.Literal["definer"]] class Attrs[T]: