From 0a5fef3b5bc171dced7439ea5a21db6121ac4090 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Fri, 6 Feb 2026 13:23:18 -0800 Subject: [PATCH 1/2] Make test_type_eval work with from __future__ import annotations --- tests/test_type_eval.py | 54 +++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 0963ab5..0ec149d 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import collections import textwrap import unittest @@ -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]: ... @@ -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]: ... @@ -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 @@ -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 From 68192a8c6f5251702ae0e3078d9bccb605892cdd Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Fri, 6 Feb 2026 14:33:57 -0800 Subject: [PATCH 2/2] Fix the tests, rebased on UpdateClass This fixes a couple actual bugs: * need to eval types in UpdateClass * evaluating strings in _eval_type_type shouldn't be needed, we'll do it when we have to with _apply_generic --- tests/test_fastapilike_1.py | 5 ++- tests/test_type_eval.py | 36 ++++++++----------- typemap/type_eval/_eval_operators.py | 44 +++++++++++++++++++---- typemap/type_eval/_eval_typing.py | 53 +--------------------------- 4 files changed, 58 insertions(+), 80 deletions(-) diff --git a/tests/test_fastapilike_1.py b/tests/test_fastapilike_1.py index 9a18b5b..ca72fd6 100644 --- a/tests/test_fastapilike_1.py +++ b/tests/test_fastapilike_1.py @@ -1,3 +1,6 @@ +# We should have at least *one* test with this... +from __future__ import annotations + import dataclasses import enum import textwrap @@ -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] diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 0ec149d..8962228 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -1827,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 @@ -1906,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], ] @@ -1963,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 @@ -1993,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 diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 38a5587..bd44085 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -7,6 +7,8 @@ import re import types import typing +import sys + from typing_extensions import _AnnotatedAlias as typing_AnnotatedAlias from typemap import type_eval @@ -14,7 +16,6 @@ from typemap.type_eval._eval_typing import ( _child_context, _eval_types, - _get_class_type_hint_namespaces, ) from typemap.typing import ( Attrs, @@ -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 @@ -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 = {} diff --git a/typemap/type_eval/_eval_typing.py b/typemap/type_eval/_eval_typing.py index 471c413..8308b92 100644 --- a/typemap/type_eval/_eval_typing.py +++ b/typemap/type_eval/_eval_typing.py @@ -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",) @@ -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