Skip to content

Commit 38ad82b

Browse files
authored
Make test_type_eval work with from __future__ import annotations (#88)
1 parent 3a9c9a0 commit 38ad82b

4 files changed

Lines changed: 89 additions & 103 deletions

File tree

tests/test_fastapilike_1.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# We should have at least *one* test with this...
2+
from __future__ import annotations
3+
14
import dataclasses
25
import enum
36
import textwrap
@@ -164,7 +167,7 @@ class Hero:
164167
HasDefault[int | None, None]
165168
] # = Field(default=None, primary_key=True)
166169

167-
name: str
170+
name: "str"
168171
age: HasDefault[int | None, None] # = Field(default=None, index=True)
169172

170173
secret_name: Hidden[str]

tests/test_type_eval.py

Lines changed: 46 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import collections
24
import textwrap
35
import unittest
@@ -398,9 +400,10 @@ def test_getmember_01():
398400
assert d == Never
399401

400402

401-
def test_getmember_02():
402-
type OnlyIntToSet[T] = set[T] if IsAssignable[T, int] else T
403+
type OnlyIntToSet[T] = set[T] if IsAssignable[T, int] else T
404+
403405

406+
def test_getmember_02():
404407
class C:
405408
def f[T](self, x: T) -> OnlyIntToSet[T]: ...
406409

@@ -423,8 +426,6 @@ def f[T](self, x: T) -> OnlyIntToSet[T]: ...
423426

424427

425428
def test_getmember_03():
426-
type OnlyIntToSet[T] = set[T] if IsAssignable[T, int] else T
427-
428429
class C:
429430
def f[T](self, x: T) -> OnlyIntToSet[T]: ...
430431

@@ -957,20 +958,24 @@ class ATree(Generic[A]):
957958
assert eval_typing(GetArg[t, ATree, Literal[1]]) == Never
958959

959960

960-
def test_eval_getarg_custom_07():
961-
# Doubly recursive generic types
962-
A = TypeVar("A")
963-
B = TypeVar("B")
961+
# Doubly recursive generic types
962+
NA = TypeVar("NA")
963+
NB = TypeVar("NB")
964+
965+
966+
class ANode(Generic[NA, NB]):
967+
val: NA | list[BNode[NA, NB]]
968+
964969

965-
class ANode(Generic[A, B]):
966-
val: A | list[BNode[A, B]]
970+
class BNode(Generic[NA, NB]):
971+
val: NB | list[ANode[NA, NB]]
967972

968-
class BNode(Generic[A, B]):
969-
val: B | list[ANode[A, B]]
970973

971-
class ABTree(Generic[A, B]):
972-
root: ANode[A, B] | BNode[A, B]
974+
class ABTree(Generic[NA, NB]):
975+
root: ANode[NA, NB] | BNode[NA, NB]
973976

977+
978+
def test_eval_getarg_custom_07():
974979
t = ABTree[int, str]
975980
assert eval_typing(GetArg[t, ABTree, Literal[0]]) is int
976981
assert eval_typing(GetArg[t, ABTree, Literal[1]]) is str
@@ -982,19 +987,22 @@ class ABTree(Generic[A, B]):
982987
assert eval_typing(GetArg[t, ABTree, Literal[2]]) == Never
983988

984989

985-
def test_eval_getarg_custom_08():
986-
# Generic class with generic methods
987-
T = TypeVar("T")
990+
T = TypeVar("T")
988991

989-
class Container(Generic[T]):
990-
data: list[T]
991992

992-
def get[T](self, index: int, default: T) -> int | T: ...
993-
def map[U](self, func: Callable[[int], U]) -> list[U]: ...
994-
def convert[T](self, func: Callable[[int], T]) -> Container2[T]: ...
993+
class Container(Generic[T]):
994+
data: list[T]
995995

996-
class Container2[T]: ...
996+
def get[T](self, index: int, default: T) -> int | T: ...
997+
def map[U](self, func: Callable[[int], U]) -> list[U]: ...
998+
def convert[T](self, func: Callable[[int], T]) -> Container2[T]: ...
997999

1000+
1001+
class Container2[T]: ...
1002+
1003+
1004+
def test_eval_getarg_custom_08():
1005+
# Generic class with generic methods
9981006
t = Container[int]
9991007
assert eval_typing(GetArg[t, Container, Literal[0]]) is int
10001008
assert eval_typing(GetArg[t, Container, Literal[-1]]) is int
@@ -1819,12 +1827,13 @@ def g(self) -> int: ... # omitted
18191827
assert m == Never
18201828

18211829

1830+
type AttrsAsSets[T] = UpdateClass[
1831+
*[Member[GetName[m], set[GetType[m]]] for m in Iter[Attrs[T]]]
1832+
]
1833+
1834+
18221835
def test_update_class_members_03():
18231836
# Generic UpdateClass, uses T
1824-
type AttrsAsSets[T] = UpdateClass[
1825-
*[Member[GetName[m], set[GetType[m]]] for m in Iter[Attrs[T]]]
1826-
]
1827-
18281837
class A:
18291838
a: int
18301839

@@ -1898,9 +1907,8 @@ def g(self) -> int: ...
18981907

18991908
# Attrs
19001909
attrs = eval_typing(Attrs[B])
1901-
assert (
1902-
attrs
1903-
== tuple[
1910+
assert str(attrs) == str(
1911+
tuple[
19041912
Member[Literal["a"], int, Never, Never, A],
19051913
Member[Literal["b"], int, Never, Never, B],
19061914
]
@@ -1955,10 +1963,6 @@ def g(self) -> int: ...
19551963

19561964
def test_update_class_inheritance_01():
19571965
# current class init subclass is not applied
1958-
type AttrsAsSets[T] = UpdateClass[
1959-
*[Member[GetName[m], set[GetType[m]]] for m in Iter[Attrs[T]]]
1960-
]
1961-
19621966
class A:
19631967
a: int
19641968

@@ -1985,18 +1989,16 @@ def __init_subclass__[T](
19851989
)
19861990

19871991

1992+
type AttrsAsList[T] = UpdateClass[
1993+
*[Member[GetName[m], list[GetType[m]]] for m in Iter[Attrs[T]]]
1994+
]
1995+
type AttrsAsTuple[T] = UpdateClass[
1996+
*[Member[GetName[m], tuple[GetType[m]]] for m in Iter[Attrs[T]]]
1997+
]
1998+
1999+
19882000
def test_update_class_inheritance_02():
19892001
# __init_subclass__ calls follow normal MRO
1990-
type AttrsAsSets[T] = UpdateClass[
1991-
*[Member[GetName[m], set[GetType[m]]] for m in Iter[Attrs[T]]]
1992-
]
1993-
type AttrsAsList[T] = UpdateClass[
1994-
*[Member[GetName[m], list[GetType[m]]] for m in Iter[Attrs[T]]]
1995-
]
1996-
type AttrsAsTuple[T] = UpdateClass[
1997-
*[Member[GetName[m], tuple[GetType[m]]] for m in Iter[Attrs[T]]]
1998-
]
1999-
20002002
class A:
20012003
a: int
20022004

typemap/type_eval/_eval_operators.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77
import re
88
import types
99
import typing
10+
import sys
11+
1012
from typing_extensions import _AnnotatedAlias as typing_AnnotatedAlias
1113

1214
from typemap import type_eval
1315
from typemap.type_eval import _apply_generic, _typing_inspect
1416
from typemap.type_eval._eval_typing import (
1517
_child_context,
1618
_eval_types,
17-
_get_class_type_hint_namespaces,
1819
)
1920
from typemap.typing import (
2021
Attrs,
@@ -180,11 +181,10 @@ def _get_update_class_members(
180181
) -> list[Member] | None:
181182
if (
182183
(init_subclass := base.__dict__.get("__init_subclass__"))
183-
and (
184-
init_subclass_annos := getattr(
185-
init_subclass, "__annotations__", None
186-
)
187-
)
184+
# XXX: We're using get_type_hints now to evaluate hints but
185+
# we should have our own generic infrastructure instead.
186+
# (I'm working on it -sully)
187+
and (init_subclass_annos := typing.get_type_hints(init_subclass))
188188
and (ret_annotation := init_subclass_annos.get("return"))
189189
):
190190
# Substitute the cls type var with the current class
@@ -751,6 +751,38 @@ def _resolved_function_signature(func, receiver_type=None):
751751
return sig
752752

753753

754+
def _get_class_type_hint_namespaces(
755+
obj: type,
756+
) -> tuple[dict[str, typing.Any], dict[str, typing.Any]]:
757+
globalns: dict[str, typing.Any] = {}
758+
localns: dict[str, typing.Any] = {}
759+
760+
# Get module globals
761+
if obj.__module__ and (module := sys.modules.get(obj.__module__)):
762+
globalns.update(module.__dict__)
763+
764+
# Annotations may use typevars defined in the class
765+
localns.update(obj.__dict__)
766+
767+
if _typing_inspect.is_generic_alias(obj):
768+
# We need the origin's type vars
769+
localns.update(obj.__origin__.__dict__)
770+
771+
# Extract type parameters from the class
772+
args = typing.get_args(obj)
773+
origin = typing.get_origin(obj)
774+
tps = getattr(obj, '__type_params__', ()) or getattr(
775+
origin, '__parameters__', ()
776+
)
777+
for tp, arg in zip(tps, args, strict=False):
778+
localns[tp.__name__] = arg
779+
780+
# Add the class itself for self-references
781+
localns[obj.__name__] = obj
782+
783+
return globalns, localns
784+
785+
754786
def _get_function_hint_namespaces(func, receiver_type=None):
755787
globalns = {}
756788
localns = {}

typemap/type_eval/_eval_typing.py

Lines changed: 1 addition & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
if typing.TYPE_CHECKING:
2323
from typing import Any, Sequence
2424

25-
from . import _apply_generic, _typing_inspect
25+
from . import _apply_generic
2626

2727

2828
__all__ = ("eval_typing",)
@@ -282,59 +282,8 @@ def _eval_func(
282282
return _apply_generic.make_func(func, annos)
283283

284284

285-
def _get_class_type_hint_namespaces(
286-
obj: type,
287-
) -> tuple[dict[str, typing.Any], dict[str, typing.Any]]:
288-
globalns: dict[str, typing.Any] = {}
289-
localns: dict[str, typing.Any] = {}
290-
291-
# Get module globals
292-
if obj.__module__ and (module := sys.modules.get(obj.__module__)):
293-
globalns.update(module.__dict__)
294-
295-
# Annotations may use typevars defined in the class
296-
localns.update(obj.__dict__)
297-
298-
if _typing_inspect.is_generic_alias(obj):
299-
# We need the origin's type vars
300-
localns.update(obj.__origin__.__dict__)
301-
302-
# Extract type parameters from the class
303-
args = typing.get_args(obj)
304-
origin = typing.get_origin(obj)
305-
tps = getattr(obj, '__type_params__', ()) or getattr(
306-
origin, '__parameters__', ()
307-
)
308-
for tp, arg in zip(tps, args, strict=False):
309-
localns[tp.__name__] = arg
310-
311-
# Add the class itself for self-references
312-
localns[obj.__name__] = obj
313-
314-
return globalns, localns
315-
316-
317285
@_eval_types_impl.register
318286
def _eval_type_type(obj: type, ctx: EvalContext):
319-
# Ensure that any string annotations are resolved
320-
if (
321-
hasattr(obj, '__annotations__')
322-
and obj.__annotations__
323-
and any(isinstance(v, str) for v in obj.__annotations__.values())
324-
):
325-
# Ensure we don't recurse infinitely
326-
ctx.seen[obj] = obj
327-
328-
# Replace string annotations with resolved types
329-
globalns, localns = _get_class_type_hint_namespaces(obj)
330-
hints = {
331-
k: _eval_types(v, ctx)
332-
for k, v in typing.get_type_hints(
333-
obj, globalns=globalns, localns=localns, include_extras=True
334-
).items()
335-
}
336-
obj.__annotations__.update(hints)
337-
338287
return obj
339288

340289

0 commit comments

Comments
 (0)