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
26 changes: 21 additions & 5 deletions tests/test_type_dir.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import typing
from typing import Literal, Never, TypeVar, TypedDict, Union, ReadOnly

from typemap.type_eval import eval_typing
from typemap.type_eval import eval_typing, _ensure_context
from typemap_extensions import (
Attrs,
FromUnion,
Expand Down Expand Up @@ -323,6 +323,25 @@ class Last[bool]:
""")


def test_type_dir_10():
class Lurr:
def foo[T](x: T) -> int if IsAssignable[T, str] else list[int]: ...

d = eval_typing(Lurr)

assert format_helper.format_class(d) == textwrap.dedent("""\
class Lurr:
foo: typing.ClassVar[typemap.typing.GenericCallable[tuple[T], <...>]]
""")

member = _get_member(eval_typing(Members[Lurr]), "foo")

fn = member.__args__[1].__args__[1]
with _ensure_context():
assert fn(str).__args__[1] is int
assert fn(bool).__args__[1] == list[int]


def test_type_dir_get_arg_1():
d = eval_typing(BaseArg[Final])
assert d is int
Expand Down Expand Up @@ -405,10 +424,7 @@ def test_type_members_func_3():
assert name == typing.Literal["sbase"]
assert quals == typing.Literal["ClassVar"]

assert (
str(typ)
== "typemap.typing.GenericCallable[tuple[Z], typemap.type_eval._eval_operators._create_generic_callable_lambda.<locals>.<lambda>]"
)
assert str(typ) == "typemap.typing.GenericCallable[tuple[Z], <...>]"

evaled = eval_typing(
typing.get_args(typ)[1](*typing.get_args(typing.get_args(typ)[0]))
Expand Down
2 changes: 2 additions & 0 deletions typemap/type_eval/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from ._eval_typing import (
eval_typing,
_get_current_context,
_ensure_context,
register_evaluator,
StuckException,
_EvalProxy,
Expand Down Expand Up @@ -28,4 +29,5 @@
"StuckException",
"_EvalProxy",
"_get_current_context",
"_ensure_context",
)
69 changes: 63 additions & 6 deletions typemap/type_eval/_apply_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from . import _eval_typing
from . import _typing_inspect


if typing.TYPE_CHECKING:
from typing import Any, Mapping

Expand Down Expand Up @@ -265,7 +266,34 @@ def get_annotations(
return rr


def _resolved_function_signature(func, args):
"""Get the signature of a function with type hints resolved to arg values"""

import typemap.typing as nt

token = nt.special_form_evaluator.set(None)
try:
sig = inspect.signature(func)
finally:
nt.special_form_evaluator.reset(token)

if hints := get_annotations(func, args):
params = []
for name, param in sig.parameters.items():
annotation = hints.get(name, param.annotation)
params.append(param.replace(annotation=annotation))

return_annotation = hints.get("return", sig.return_annotation)
sig = sig.replace(
parameters=params, return_annotation=return_annotation
)

return sig


def get_local_defns(boxed: Boxed) -> tuple[dict[str, Any], dict[str, Any]]:
from typemap.typing import GenericCallable

annos: dict[str, Any] = {}
dct: dict[str, Any] = {}

Expand All @@ -284,25 +312,54 @@ def get_local_defns(boxed: Boxed) -> tuple[dict[str, Any], dict[str, Any]]:
# TODO: This annos_ok thing is a hack because processing
# __annotations__ on methods broke stuff and I didn't want
# to chase it down yet.
stuck = False
try:
rr = get_annotations(
stuff, boxed.str_args, cls=boxed.cls, annos_ok=False
)
except _eval_typing.StuckException:
# TODO: Either generate a GenericCallable or a
# function with our own __annotate__ for this case
# where we can't even fetch the signature without
# trouble.
stuck = True
rr = None

if rr is not None:
local_fn = make_func(orig, rr)
elif getattr(stuff, "__annotations__", None):
elif not stuck and getattr(stuff, "__annotations__", None):
# XXX: This is totally wrong; we still need to do
# substitute in class vars
local_fn = stuff

if local_fn is not None:
# 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__
str_args = boxed.str_args

def _make_lambda(fn, o, sa, tp):
from ._eval_operators import _function_type_from_sig

def lam(*vs):
args = dict(sa)
args.update(
zip(
(str(p) for p in tp),
vs,
strict=True,
)
)
sig = _resolved_function_signature(fn, args)
return _function_type_from_sig(
sig, o, receiver_type=None
)

return lam

gc = GenericCallable[ # type: ignore[valid-type,misc]
tuple[*type_params], # type: ignore[valid-type]
_make_lambda(stuff, orig, str_args, type_params),
]
annos[name] = typing.ClassVar[gc]
elif local_fn is not None:
if orig.__class__ is classmethod:
local_fn = classmethod(local_fn)
elif orig.__class__ is staticmethod:
Expand Down
30 changes: 22 additions & 8 deletions typemap/type_eval/_eval_operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def cached_box(cls, *, ctx):
return box


def get_annotated_type_hints(cls, *, ctx, **kwargs):
def get_annotated_type_hints(cls, *, ctx, attrs_only=False, **kwargs):
"""Get the type hints/quals for a cls annotated with definition site.

This traverses the mro and finds the definition site for each annotation.
Expand Down Expand Up @@ -127,6 +127,10 @@ def get_annotated_type_hints(cls, *, ctx, **kwargs):
else:
break

# Skip method-like ClassVars when only attributes are wanted
if attrs_only and "ClassVar" in quals and _is_method_like(ty):
continue

if k in abox.cls.__dict__:
# Wrap in tuple when creating Literal in case it *is* a tuple
init = _make_init_type(abox.cls.__dict__[k])
Expand Down Expand Up @@ -647,11 +651,7 @@ def _callable_type_to_method(name, typ, ctx):
return head(func)


def _function_type(func, *, receiver_type):
root = inspect.unwrap(func)
sig = inspect.signature(root)
# XXX: __type_params__!!!

def _function_type_from_sig(sig, func, *, receiver_type):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could maybe replace func with func_type

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not, though; it's the actual function?

Oh, or did you mean pass in type(func) instead?

empty = inspect.Parameter.empty

def _ann(x):
Expand Down Expand Up @@ -707,6 +707,15 @@ def _ann(x):
f = classmethod[specified_receiver, tuple[*params[1:]], ret]
else:
f = typing.Callable[params, ret]

return f


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__
Expand Down Expand Up @@ -762,7 +771,9 @@ def _hints_to_members(hints, ctx):
@type_eval.register_evaluator(Attrs)
@_lift_over_unions
def _eval_Attrs(tp, *, ctx):
hints = get_annotated_type_hints(tp, include_extras=True, ctx=ctx)
hints = get_annotated_type_hints(
tp, include_extras=True, attrs_only=True, ctx=ctx
)
return _hints_to_members(hints, ctx)


Expand Down Expand Up @@ -1190,7 +1201,10 @@ def _eval_NewProtocol(*etyps: Member, ctx):
if type_eval.issubtype(
typing.Literal["ClassVar"], tquals
) and _is_method_like(typ):
dct[name] = _callable_type_to_method(name, typ, ctx)
try:
dct[name] = _callable_type_to_method(name, typ, ctx)
except type_eval.StuckException:
annos[name] = _add_quals(typ, tquals)
else:
annos[name] = _add_quals(typ, tquals)
_unpack_init(dct, name, init)
Expand Down
12 changes: 11 additions & 1 deletion typemap/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,17 @@ class SpecialFormEllipsis:


class _GenericCallableGenericAlias(_GenericAlias, _root=True):
pass
def __repr__(self):
from typing import _type_repr

name = _type_repr(self.__origin__)
if self.__args__:
rargs = [_type_repr(self.__args__[0]), "<...>"]
args = ", ".join(rargs)
else:
# To ensure the repr is eval-able.
args = "()"
return f'{name}[{args}]'


class GenericCallable:
Expand Down