diff --git a/spec-draft.rst b/spec-draft.rst index 0fb30b7..e5279b4 100644 --- a/spec-draft.rst +++ b/spec-draft.rst @@ -1,3 +1,11 @@ +A minor proposal that could be split out maybe: + +Supporting ``Unpack`` of typevars for ``*kwargs`` + + + + +----------------------------------------------------------------------- Grammar specification of the extensions to the type language. diff --git a/tests/test_call.py b/tests/test_call.py index 2138404..0cdbe9c 100644 --- a/tests/test_call.py +++ b/tests/test_call.py @@ -1,23 +1,24 @@ import textwrap +from typing import Unpack + from typemap.type_eval import eval_call from typemap.typing import ( - CallSpec, + Attrs, + BaseTypedDict, NewProtocol, Member, GetName, Iter, - CallSpecKwargs, ) from . import format_helper -def func[C: CallSpec]( - *args: C.args, **kwargs: C.kwargs -) -> NewProtocol[ - *[Member[GetName[c], int] for c in Iter[CallSpecKwargs[C]]] -]: ... +def func[*T, K: BaseTypedDict]( + *args: Unpack[T], + **kwargs: Unpack[K], +) -> NewProtocol[*[Member[GetName[c], int] for c in Iter[Attrs[K]]]]: ... def test_call_1(): @@ -30,3 +31,24 @@ class func[...]: b: int c: int """) + + +def func_trivial[*T, K: BaseTypedDict]( + *args: Unpack[T], + **kwargs: Unpack[K], +) -> K: + return kwargs + + +def test_call_2(): + ret = eval_call(func_trivial, a=1, b=2, c="aaa") + fmt = format_helper.format_class(ret) + + # XXX: can we get rid of the annotate?? + assert fmt == textwrap.dedent("""\ + class **kwargs: + a: typing.Literal[1] + b: typing.Literal[2] + c: typing.Literal['aaa'] + def __annotate__(format): ... + """) diff --git a/tests/test_qblike.py b/tests/test_qblike.py index 8870589..87752d1 100644 --- a/tests/test_qblike.py +++ b/tests/test_qblike.py @@ -1,19 +1,18 @@ import textwrap -from typing import Literal +from typing import Literal, Unpack from typemap.type_eval import eval_call, eval_typing from typemap.typing import ( + BaseTypedDict, NewProtocol, Iter, Attrs, Is, GetType, - CallSpec, Member, GetName, GetAttr, - CallSpecKwargs, GetArg, ) @@ -36,6 +35,20 @@ class Link[T]: type FilterLinks[T] = Link[PropsOnly[GetArg[T, Link, 0]]] if Is[T, Link] else T +def select[K: BaseTypedDict]( + __rcv: A, + **kwargs: Unpack[K], +) -> NewProtocol[ + *[ + Member[ + GetName[c], + FilterLinks[GetAttr[A, GetName[c]]], + ] + for c in Iter[Attrs[K]] + ] +]: ... + + # Basic filtering class Tgt2: pass @@ -53,19 +66,6 @@ class A: w: Property[list[str]] -def select[C: CallSpec]( - __rcv: A, *args: C.args, **kwargs: C.kwargs -) -> NewProtocol[ - *[ - Member[ - GetName[c], - FilterLinks[GetAttr[A, GetName[c]]], - ] - for c in Iter[CallSpecKwargs[C]] - ] -]: ... - - def test_qblike_1(): ret = eval_call( select, diff --git a/typemap/type_eval/_eval_call.py b/typemap/type_eval/_eval_call.py index 248030d..e00de08 100644 --- a/typemap/type_eval/_eval_call.py +++ b/typemap/type_eval/_eval_call.py @@ -1,38 +1,102 @@ import annotationlib +import enum +import inspect import types import typing +import typing_extensions -if typing.TYPE_CHECKING: - from typing import Any +from typing import Any -from typemap import typing as next from . import _eval_typing +RtType = Any -def eval_call(func: types.FunctionType, /, *args: Any, **kwargs: Any) -> Any: - with _eval_typing._ensure_context() as ctx: - return _eval_call(func, ctx, *args, **kwargs) +from typing import _UnpackGenericAlias # type: ignore [attr-defined] # noqa: PLC2701 + + +def _type(t): + if t is None or isinstance(t, (int, str, bool, bytes, enum.Enum)): + return typing.Literal[t] + else: + return type(t) + + +def eval_call(func: types.FunctionType, /, *args: Any, **kwargs: Any) -> RtType: + arg_types = tuple(_type(t) for t in args) + kwarg_types = {k: _type(t) for k, t in kwargs.items()} + return eval_call_with_types(func, arg_types, kwarg_types) -def _eval_call( +def _get_bound_type_args( func: types.FunctionType, - ctx: _eval_typing.EvalContext, - /, - *args: Any, - **kwargs: Any, -) -> Any: - vars: dict[str, Any] = {} + arg_types: tuple[RtType, ...], + kwarg_types: dict[str, RtType], +) -> dict[str, RtType]: + sig = inspect.signature(func) + bound = sig.bind(*arg_types, **kwarg_types) + + vars: dict[str, RtType] = {} + # TODO: duplication, error cases + for param in sig.parameters.values(): + if ( + param.kind == inspect.Parameter.VAR_POSITIONAL + # XXX: typing_extensions also + and isinstance(param.annotation, _UnpackGenericAlias) + and param.annotation.__args__ + and (tv := param.annotation.__args__[0]) + # XXX: should we allow just a regular one with a tuple bound also? + # maybe! it would match what I want to do for kwargs! + and isinstance(tv, typing.TypeVarTuple) + ): + tps = bound.arguments.get(param.name, ()) + vars[tv.__name__] = tuple[tps] # type: ignore[valid-type] + elif ( + param.kind == inspect.Parameter.VAR_KEYWORD + # XXX: typing_extensions also + and isinstance(param.annotation, _UnpackGenericAlias) + and param.annotation.__args__ + and (tv := param.annotation.__args__[0]) + # XXX: should we allow just a regular one with a tuple bound also? + # maybe! it would match what I want to do for kwargs! + and isinstance(tv, typing.TypeVar) + and tv.__bound__ + and typing_extensions.is_typeddict(tv.__bound__) + ): + tp = typing.TypedDict(f"**{param.name}", bound.kwargs) # type: ignore[misc, operator] + vars[tv.__name__] = tp + # TODO: simple bindings to other variables too + + return vars + +def eval_call_with_types( + func: types.FunctionType, + arg_types: tuple[RtType, ...], + kwarg_types: dict[str, RtType], +) -> RtType: + vars: dict[str, Any] = {} params = func.__type_params__ + vars = _get_bound_type_args(func, arg_types, kwarg_types) for p in params: - if hasattr(p, "__bound__") and p.__bound__ is next.CallSpec: - vars[p.__name__] = next._CallSpecWrapper( - args, tuple(kwargs.items()), func - ) - else: + if p.__name__ not in vars: vars[p.__name__] = p + return eval_call_with_type_vars(func, vars) + + +def eval_call_with_type_vars( + func: types.FunctionType, vars: dict[str, RtType] +) -> RtType: + with _eval_typing._ensure_context() as ctx: + return _eval_call_with_type_vars(func, vars, ctx) + + +def _eval_call_with_type_vars( + func: types.FunctionType, + vars: dict[str, RtType], + ctx: _eval_typing.EvalContext, +) -> RtType: try: af = func.__annotate__ except AttributeError: diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 0d80289..ad5ff46 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -10,8 +10,6 @@ from typemap.typing import ( Attrs, - CallSpecKwargs, - _CallSpecWrapper, Iter, IsSubtype, IsSubSimilar, @@ -123,38 +121,6 @@ def _eval_IsSubSimilar(lhs, rhs, *, ctx): ################################################################## -@type_eval.register_evaluator(CallSpecKwargs) -def _eval_CallSpecKwargs(spec: _CallSpecWrapper, *, ctx): - ff = types.FunctionType( - spec._func.__code__, - spec._func.__globals__, - spec._func.__name__, - None, - (), - ) - - # We can't call `inspect.signature` on `spec` directly -- - # signature() will attempt to resolve annotations and fail. - # So we run it on a copy of the function that doesn't have - # annotations set. - sig = inspect.signature(ff) - bound = sig.bind(*spec._args, **dict(spec._kwargs)) - - # TODO: Get the real type instead of Never - return tuple[ # type: ignore[misc] - *[ - Member[ - typing.Literal[name], # type: ignore[valid-type] - typing.Never, - ] - for name in bound.kwargs - ] - ] - - -################################################################## - - def _function_type(func, *, is_method): root = inspect.unwrap(func) sig = inspect.signature(root) diff --git a/typemap/typing.py b/typemap/typing.py index 5b08b87..9d4eccb 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -1,40 +1,17 @@ -from dataclasses import dataclass - import contextvars -import types import typing from typing import _GenericAlias # type: ignore - _SpecialForm: typing.Any = typing._SpecialForm +# Not type-level computation but related -@dataclass(frozen=True) -class CallSpec: - pass - - -@dataclass(frozen=True) -class _CallSpecWrapper: - _args: tuple[typing.Any] - _kwargs: tuple[tuple[str, typing.Any], ...] - # TODO: Support MethodType! - _func: types.FunctionType # | types.MethodType - @property - def args(self) -> None: - pass - - @property - def kwargs(self) -> None: - pass - - -class CallSpecKwargs[Spec]: +class BaseTypedDict(typing.TypedDict): pass -################################################################## +### class Member[N: str, T, Q: str = typing.Never, D = typing.Never]: