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
5 changes: 4 additions & 1 deletion tests/test_fastapilike_1.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# We should have at least *one* test with this...
from __future__ import annotations

import dataclasses
import enum
import textwrap
Expand Down Expand Up @@ -164,7 +167,7 @@ class Hero:
HasDefault[int | None, None]
] # = Field(default=None, primary_key=True)

name: str
name: "str"
age: HasDefault[int | None, None] # = Field(default=None, index=True)

secret_name: Hidden[str]
Expand Down
90 changes: 46 additions & 44 deletions tests/test_type_eval.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import collections
import textwrap
import unittest
Expand Down Expand Up @@ -398,9 +400,10 @@ def test_getmember_01():
assert d == Never


def test_getmember_02():
type OnlyIntToSet[T] = set[T] if IsAssignable[T, int] else T
type OnlyIntToSet[T] = set[T] if IsAssignable[T, int] else T


def test_getmember_02():
class C:
def f[T](self, x: T) -> OnlyIntToSet[T]: ...

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


def test_getmember_03():
type OnlyIntToSet[T] = set[T] if IsAssignable[T, int] else T

class C:
def f[T](self, x: T) -> OnlyIntToSet[T]: ...

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


def test_eval_getarg_custom_07():
# Doubly recursive generic types
A = TypeVar("A")
B = TypeVar("B")
# Doubly recursive generic types
NA = TypeVar("NA")
NB = TypeVar("NB")


class ANode(Generic[NA, NB]):
val: NA | list[BNode[NA, NB]]


class ANode(Generic[A, B]):
val: A | list[BNode[A, B]]
class BNode(Generic[NA, NB]):
val: NB | list[ANode[NA, NB]]

class BNode(Generic[A, B]):
val: B | list[ANode[A, B]]

class ABTree(Generic[A, B]):
root: ANode[A, B] | BNode[A, B]
class ABTree(Generic[NA, NB]):
root: ANode[NA, NB] | BNode[NA, NB]


def test_eval_getarg_custom_07():
t = ABTree[int, str]
assert eval_typing(GetArg[t, ABTree, Literal[0]]) is int
assert eval_typing(GetArg[t, ABTree, Literal[1]]) is str
Expand All @@ -982,19 +987,22 @@ class ABTree(Generic[A, B]):
assert eval_typing(GetArg[t, ABTree, Literal[2]]) == Never


def test_eval_getarg_custom_08():
# Generic class with generic methods
T = TypeVar("T")
T = TypeVar("T")

class Container(Generic[T]):
data: list[T]

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

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


class Container2[T]: ...


def test_eval_getarg_custom_08():
# Generic class with generic methods
t = Container[int]
assert eval_typing(GetArg[t, Container, Literal[0]]) is int
assert eval_typing(GetArg[t, Container, Literal[-1]]) is int
Expand Down Expand Up @@ -1819,12 +1827,13 @@ def g(self) -> int: ... # omitted
assert m == Never


type AttrsAsSets[T] = UpdateClass[
*[Member[GetName[m], set[GetType[m]]] for m in Iter[Attrs[T]]]
]


def test_update_class_members_03():
# Generic UpdateClass, uses T
type AttrsAsSets[T] = UpdateClass[
*[Member[GetName[m], set[GetType[m]]] for m in Iter[Attrs[T]]]
]

class A:
a: int

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

# Attrs
attrs = eval_typing(Attrs[B])
assert (
attrs
== tuple[
assert str(attrs) == str(
tuple[
Member[Literal["a"], int, Never, Never, A],
Member[Literal["b"], int, Never, Never, B],
]
Expand Down Expand Up @@ -1955,10 +1963,6 @@ def g(self) -> int: ...

def test_update_class_inheritance_01():
# current class init subclass is not applied
type AttrsAsSets[T] = UpdateClass[
*[Member[GetName[m], set[GetType[m]]] for m in Iter[Attrs[T]]]
]

class A:
a: int

Expand All @@ -1985,18 +1989,16 @@ def __init_subclass__[T](
)


type AttrsAsList[T] = UpdateClass[
*[Member[GetName[m], list[GetType[m]]] for m in Iter[Attrs[T]]]
]
type AttrsAsTuple[T] = UpdateClass[
*[Member[GetName[m], tuple[GetType[m]]] for m in Iter[Attrs[T]]]
]


def test_update_class_inheritance_02():
# __init_subclass__ calls follow normal MRO
type AttrsAsSets[T] = UpdateClass[
*[Member[GetName[m], set[GetType[m]]] for m in Iter[Attrs[T]]]
]
type AttrsAsList[T] = UpdateClass[
*[Member[GetName[m], list[GetType[m]]] for m in Iter[Attrs[T]]]
]
type AttrsAsTuple[T] = UpdateClass[
*[Member[GetName[m], tuple[GetType[m]]] for m in Iter[Attrs[T]]]
]

class A:
a: int

Expand Down
44 changes: 38 additions & 6 deletions typemap/type_eval/_eval_operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
import re
import types
import typing
import sys

from typing_extensions import _AnnotatedAlias as typing_AnnotatedAlias

from typemap import type_eval
from typemap.type_eval import _apply_generic, _typing_inspect
from typemap.type_eval._eval_typing import (
_child_context,
_eval_types,
_get_class_type_hint_namespaces,
)
from typemap.typing import (
Attrs,
Expand Down Expand Up @@ -180,11 +181,10 @@ def _get_update_class_members(
) -> list[Member] | None:
if (
(init_subclass := base.__dict__.get("__init_subclass__"))
and (
init_subclass_annos := getattr(
init_subclass, "__annotations__", None
)
)
# XXX: We're using get_type_hints now to evaluate hints but
# we should have our own generic infrastructure instead.
# (I'm working on it -sully)
and (init_subclass_annos := typing.get_type_hints(init_subclass))
and (ret_annotation := init_subclass_annos.get("return"))
):
# Substitute the cls type var with the current class
Expand Down Expand Up @@ -751,6 +751,38 @@ def _resolved_function_signature(func, receiver_type=None):
return sig


def _get_class_type_hint_namespaces(
obj: type,
) -> tuple[dict[str, typing.Any], dict[str, typing.Any]]:
globalns: dict[str, typing.Any] = {}
localns: dict[str, typing.Any] = {}

# Get module globals
if obj.__module__ and (module := sys.modules.get(obj.__module__)):
globalns.update(module.__dict__)

# Annotations may use typevars defined in the class
localns.update(obj.__dict__)

if _typing_inspect.is_generic_alias(obj):
# We need the origin's type vars
localns.update(obj.__origin__.__dict__)

# Extract type parameters from the class
args = typing.get_args(obj)
origin = typing.get_origin(obj)
tps = getattr(obj, '__type_params__', ()) or getattr(
origin, '__parameters__', ()
)
for tp, arg in zip(tps, args, strict=False):
localns[tp.__name__] = arg

# Add the class itself for self-references
localns[obj.__name__] = obj

return globalns, localns


def _get_function_hint_namespaces(func, receiver_type=None):
globalns = {}
localns = {}
Expand Down
53 changes: 1 addition & 52 deletions typemap/type_eval/_eval_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
if typing.TYPE_CHECKING:
from typing import Any, Sequence

from . import _apply_generic, _typing_inspect
from . import _apply_generic


__all__ = ("eval_typing",)
Expand Down Expand Up @@ -282,59 +282,8 @@ def _eval_func(
return _apply_generic.make_func(func, annos)


def _get_class_type_hint_namespaces(
obj: type,
) -> tuple[dict[str, typing.Any], dict[str, typing.Any]]:
globalns: dict[str, typing.Any] = {}
localns: dict[str, typing.Any] = {}

# Get module globals
if obj.__module__ and (module := sys.modules.get(obj.__module__)):
globalns.update(module.__dict__)

# Annotations may use typevars defined in the class
localns.update(obj.__dict__)

if _typing_inspect.is_generic_alias(obj):
# We need the origin's type vars
localns.update(obj.__origin__.__dict__)

# Extract type parameters from the class
args = typing.get_args(obj)
origin = typing.get_origin(obj)
tps = getattr(obj, '__type_params__', ()) or getattr(
origin, '__parameters__', ()
)
for tp, arg in zip(tps, args, strict=False):
localns[tp.__name__] = arg

# Add the class itself for self-references
localns[obj.__name__] = obj

return globalns, localns


@_eval_types_impl.register
def _eval_type_type(obj: type, ctx: EvalContext):
# Ensure that any string annotations are resolved
if (
hasattr(obj, '__annotations__')
and obj.__annotations__
and any(isinstance(v, str) for v in obj.__annotations__.values())
):
# Ensure we don't recurse infinitely
ctx.seen[obj] = obj

# Replace string annotations with resolved types
globalns, localns = _get_class_type_hint_namespaces(obj)
hints = {
k: _eval_types(v, ctx)
for k, v in typing.get_type_hints(
obj, globalns=globalns, localns=localns, include_extras=True
).items()
}
obj.__annotations__.update(hints)

return obj


Expand Down