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
16 changes: 11 additions & 5 deletions tests/format_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@
import typing


def format_class(cls: type) -> str:
def format_meth(meth):
def format_class_basic(cls: type) -> str:
def format_meth(name, meth):
root = inspect.unwrap(meth)
sig = inspect.signature(root)

ts = ""
if params := root.__type_params__:
ts = "[" + ", ".join(str(p) for p in params) + "]"

return f"{root.__name__}{ts}{sig}"
return f"{name}{ts}{sig}"

code = f"class {cls.__name__}:\n"
for attr_name, attr_type in cls.__annotations__.items():
attr_type_s = annotationlib.type_repr(attr_type)
code += f" {attr_name}: {attr_type_s}\n"

for attr in cls.__dict__.values():
for name, attr in cls.__dict__.items():
if attr is typing._no_init_or_replace_init:
continue
if isinstance(attr, classmethod):
Expand All @@ -32,5 +32,11 @@ def format_meth(meth):
# Intentionally not elif; classmethod and staticmethod cases
# fall through
if isinstance(attr, (types.FunctionType, types.MethodType)):
code += f" def {format_meth(attr)}: ...\n"
code += f" def {format_meth(name, attr)}: ...\n"
return code


def format_class(cls):
from typemap.type_eval import flatten_class

return format_class_basic(flatten_class(cls))
2 changes: 0 additions & 2 deletions tests/test_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,9 @@ 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): ...
""")
4 changes: 1 addition & 3 deletions tests/test_qblike.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,8 @@ def test_qblike_3():
class select[...]:
x: tests.test_qblike.Property[int]
w: tests.test_qblike.Property[list[str]]
z: tests.test_qblike.Link[PropsOnly[typemap.typing.GetArg[\
tests.test_qblike.Link[tests.test_qblike.Tgt], tests.test_qblike.Link, 0]]]
z: tests.test_qblike.Link[tests.test_qblike.PropsOnly[tests.test_qblike.Tgt]]
""")
# z: tests.test_qblike.Link[PropsOnly[tests.test_qblike.Tgt]]

res = eval_typing(GetAttr[ret, Literal["z"]])
tgt = res.__args__[0]
Expand Down
51 changes: 39 additions & 12 deletions tests/test_type_dir.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ class Final(Mine, Ordinary, Wrapper[float], AnotherBase[float], Last[int]):
]


# Subtyping this forces real type evaluation
# Subtyping Eval used to do something
class Eval[T]:
pass

Expand All @@ -175,14 +175,14 @@ def test_type_dir_link_1():
d = eval_typing(Loop)
loop = d.__annotations__["loop"]
assert loop is d
assert loop is not Foo
assert loop is Loop


def test_type_dir_link_2():
d = eval_typing(Foo)
loop = d.__annotations__["bar"].__annotations__["foo"]
assert loop is d
assert loop is not Foo
assert loop is Foo


def test_type_dir_1():
Expand Down Expand Up @@ -280,11 +280,6 @@ class NoLiterals2[tests.test_type_dir.Final]:
""")


def test_type_dir_7():
d = eval_typing(BaseArg[Final])
assert d is int


class Simple[T]:
simple: T

Expand All @@ -297,7 +292,7 @@ class Funny2(Funny[int]):
pass


def test_type_dir_8():
def test_type_dir_7():
d = eval_typing(Funny2)

assert format_helper.format_class(d) == textwrap.dedent("""\
Expand All @@ -306,21 +301,53 @@ class Funny2:
""")


def test_type_dir_9():
d = eval_typing(Last[bool])

assert format_helper.format_class(d) == textwrap.dedent("""\
class Last[bool]:
last: bool | typing.Literal[True]
""")


def test_type_dir_get_arg_1():
d = eval_typing(BaseArg[Final])
assert d is int


def _get_member(members, name):
return next(
iter(m for m in members.__args__ if m.__args__[0].__args__[0] == name)
)


def test_type_members_attr_():
def test_type_members_attr_1():
d = eval_typing(Members[Final])
member = _get_member(d, "ordinary")
assert typing.get_origin(member) is Member
_, _, _, origin = typing.get_args(member)
assert origin.__name__ == "Ordinary"


def test_type_members_func_1a():
def test_type_members_attr_2():
d = eval_typing(Members[Final])
member = _get_member(d, "last")
assert typing.get_origin(member) is Member
_, typ, _, origin = typing.get_args(member)
assert typ == int | Literal[True]
assert str(origin) == "tests.test_type_dir.Last[int]"


def test_type_members_attr_3():
d = eval_typing(Members[Last[int]])
member = _get_member(d, "last")
assert typing.get_origin(member) is Member
_, typ, _, origin = typing.get_args(member)
assert typ == int | Literal[True]
assert str(origin) == "tests.test_type_dir.Last[int]"


def test_type_members_func_1():
d = eval_typing(Members[Final])
member = _get_member(d, "foo")
assert typing.get_origin(member) is Member
Expand All @@ -339,7 +366,7 @@ def test_type_members_func_1a():
dict[str, int]]"
)

assert origin.__name__ == "Base[int]"
assert str(origin) == "tests.test_type_dir.Base[int]"


def test_type_members_func_2():
Expand Down
36 changes: 35 additions & 1 deletion tests/test_type_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,18 @@ def test_eval_types_2():
# # Validate that recursion worked properly and "Recursive" was only walked once
# assert evaled.__annotations__["a"].__args__[0] is evaled

assert format_helper.format_class(evaled) == textwrap.dedent("""\
# XXX: I don't have a good intuition about whether the inner MapRecursive ought to expand or not.
#
# Currently there are two test implementations for flatten_class
# and the canonical one does not expand it and the
# NewProtocol-based one does.
#
# I don't really think they ought to differ; something funny is
# going on with recursively alias handling.
res = format_helper.format_class(evaled)
res = res.replace('tests.test_type_eval.MapRecursive', 'MapRecursive')

assert res == textwrap.dedent("""\
class MapRecursive[tests.test_type_eval.Recursive]:
n: int | typing.Literal['gotcha!']
m: str | typing.Literal['gotcha!']
Expand Down Expand Up @@ -641,6 +652,21 @@ class Container2[T]: ...
assert eval_typing(GetArg[t, Container, 1]) == Never


type _Works[Ts, I] = Literal[True]
type Works[Ts] = _Works[Ts, Length[Ts]]

type _Fails[Ts, I] = Literal[False]
type Fails[Ts] = _Fails[Ts, Literal[0]]


def test_consistency_01():
t = eval_typing(Works[tuple[int, str]])
assert t == Literal[True]

t = eval_typing(Fails[tuple[int, str]])
assert t == Literal[False]


def test_uppercase_never():
d = eval_typing(Uppercase[Never])
assert d is Never
Expand Down Expand Up @@ -696,3 +722,11 @@ def test_eval_length_01():

d = eval_typing(Length[tuple[int, ...]])
assert d == Literal[None]


def test_eval_literal_idempotent_01():
t = Literal[int]
for _ in range(5):
nt = eval_typing(t)
assert t == nt
t = nt
2 changes: 2 additions & 0 deletions typemap/type_eval/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
register_evaluator,
_EvalProxy,
)
from ._apply_generic import flatten_class

# XXX: this needs to go second due to nasty circularity -- try to fix that!!
from ._eval_call import eval_call
Expand All @@ -18,6 +19,7 @@
"eval_typing",
"register_evaluator",
"eval_call",
"flatten_class",
"issubtype",
"issubsimilar",
"_EvalProxy",
Expand Down
49 changes: 41 additions & 8 deletions typemap/type_eval/_apply_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def substitute(ty, args):


def box(cls: type[Any]) -> Boxed:
# TODO: We want a cache for this!!
def _box(cls: type[Any], args: dict[Any, Any]) -> Boxed:
boxed_bases: list[Boxed] = []

Expand Down Expand Up @@ -106,9 +107,6 @@ def _box(cls: type[Any], args: dict[Any, Any]) -> Boxed:
return Boxed(cls, boxed_bases, args)

if isinstance(cls, (typing._GenericAlias, types.GenericAlias)): # type: ignore[attr-defined]
# XXX this feels out of place, `box()` needs to only accept types.
# this never gets activated now, but I want to basically
# support this later -sully
args = dict(
zip(cls.__origin__.__parameters__, cls.__args__, strict=True)
)
Expand Down Expand Up @@ -194,11 +192,10 @@ def _get_closure_types(af: types.FunctionType) -> dict[str, type]:
for name, variable in zip(
af.__code__.co_freevars, af.__closure__, strict=True
)
if isinstance(variable.cell_contents, type)
}


def _get_local_defns(boxed: Boxed) -> tuple[dict[str, Any], dict[str, Any]]:
def get_local_defns(boxed: Boxed) -> tuple[dict[str, Any], dict[str, Any]]:
annos: dict[str, Any] = {}
dct: dict[str, Any] = {}

Expand Down Expand Up @@ -237,6 +234,7 @@ def _get_local_defns(boxed: Boxed) -> tuple[dict[str, Any], dict[str, Any]]:
else:
annos[k] = v
elif af := getattr(boxed.cls, "__annotations__", None):
# TODO: substitute vars in this case
annos.update(af)

for name, orig in boxed.cls.__dict__.items():
Expand Down Expand Up @@ -283,6 +281,31 @@ def _get_local_defns(boxed: Boxed) -> tuple[dict[str, Any], dict[str, Any]]:
return annos, dct


def flatten_class_new_proto(cls: type) -> type:
Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't see this used anywhere?

# This is a hacky version of flatten_class that works by using
# NewProtocol on Members!
#
# It works except for methods, since NewProtocol doesn't understand those.
from typemap.typing import (
Iter,
Members,
NewProtocol,
)

type ClsAlias = NewProtocol[*[m for m in Iter[Members[cls]]]] # type: ignore[valid-type]
nt = _eval_typing.eval_typing(ClsAlias)

args = typing.get_args(cls)
args_str = ", ".join(_type_repr(a) for a in args)
args_str = f'[{args_str}]' if args_str else ''

nt.__name__ = f'{cls.__name__}{args_str}'
nt.__qualname__ = f'{cls.__qualname__}{args_str}'
del nt.__subclasshook__

return nt


def _type_repr(t: Any) -> str:
if isinstance(t, type):
if t.__module__ == "builtins":
Expand All @@ -293,7 +316,9 @@ def _type_repr(t: Any) -> str:
return repr(t)


def apply(
# TODO: Potentially most of this could be ripped out. The internals
# don't use this at all, it's only used by format_class.
def _flatten_class_explicit(
cls: type[Any], ctx: _eval_typing.EvalContext
) -> type[_eval_typing._EvalProxy]:
cls_boxed = box(cls)
Expand Down Expand Up @@ -330,13 +355,13 @@ def apply(
"__local_args__": args,
},
)
ctx.seen[boxed.alias_type()] = new[boxed] = cboxed
new[boxed] = cboxed

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

cboxed.__local_annotations__, cboxed.__local_defns__ = _get_local_defns(
cboxed.__local_annotations__, cboxed.__local_defns__ = get_local_defns(
boxed
)
for base in reversed(boxed.mro):
Expand Down Expand Up @@ -364,3 +389,11 @@ def apply(
setattr(cboxed, k, _eval_typing._eval_types(v, ctx=ctx))

return new[cls_boxed]


def flatten_class_explicit(obj: typing.Any):
with _eval_typing._ensure_context() as ctx:
return _flatten_class_explicit(obj, ctx)


flatten_class = flatten_class_explicit
Loading