Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions spec-draft.rst
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
36 changes: 29 additions & 7 deletions tests/test_call.py
Original file line number Diff line number Diff line change
@@ -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():
Expand All @@ -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): ...
""")
32 changes: 16 additions & 16 deletions tests/test_qblike.py
Original file line number Diff line number Diff line change
@@ -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,
)

Expand All @@ -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
Expand All @@ -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,
Expand Down
100 changes: 82 additions & 18 deletions typemap/type_eval/_eval_call.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
34 changes: 0 additions & 34 deletions typemap/type_eval/_eval_operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@

from typemap.typing import (
Attrs,
CallSpecKwargs,
_CallSpecWrapper,
Iter,
IsSubtype,
IsSubSimilar,
Expand Down Expand Up @@ -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)
Expand Down
29 changes: 3 additions & 26 deletions typemap/typing.py
Original file line number Diff line number Diff line change
@@ -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]:
Expand Down