From 1d54f5c1edc1fab6cf2391a5c22fb5a8116ed638 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 7 Jan 2026 15:13:55 -0800 Subject: [PATCH 1/7] Make _apply_generic return a type instead of a dict --- typemap/type_eval/_apply_generic.py | 26 ++++++++++++++++++++++---- typemap/type_eval/_eval_typing.py | 22 +++------------------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/typemap/type_eval/_apply_generic.py b/typemap/type_eval/_apply_generic.py index 4fd60fb..86443c4 100644 --- a/typemap/type_eval/_apply_generic.py +++ b/typemap/type_eval/_apply_generic.py @@ -159,12 +159,26 @@ def make_func( return new_func -def apply(cls: type[Any]) -> dict[str, Any]: +def apply( + cls: type[Any], ctx: _eval_typing.EvalContext +) -> type[_eval_typing._EvalProxy]: mro_boxed = compute_mro(cls) annos: dict[str, Any] = {} dct: dict[str, Any] = {} + # We create it early so we can add it to seen, to handle recursion + ctx.seen[cls] = ret = type( + cls.__name__, + (_eval_typing._EvalProxy,), + { + "__module__": cls.__module__, + "__name__": cls.__name__, + "__origin__": cls, + }, + ) + + # Run through the mro for boxed in reversed(mro_boxed): if af := getattr(boxed.cls, "__annotate__", None): # Class has annotations, let's resolve generic arguments @@ -235,11 +249,15 @@ def apply(cls: type[Any]) -> dict[str, Any]: dct[name] = stuff for k, v in annos.items(): - annos[k] = _eval_typing.eval_typing(v) + annos[k] = _eval_typing._eval_types(v, ctx=ctx) for k, v in dct.items(): - dct[k] = _eval_typing.eval_typing(v) + dct[k] = _eval_typing._eval_types(v, ctx=ctx) dct["__annotations__"] = annos dct["__generalized_mro__"] = mro_boxed - return dct + + for k, v in dct.items(): + setattr(ret, k, v) + + return ret diff --git a/typemap/type_eval/_eval_typing.py b/typemap/type_eval/_eval_typing.py index 7862d24..fa980e6 100644 --- a/typemap/type_eval/_eval_typing.py +++ b/typemap/type_eval/_eval_typing.py @@ -256,29 +256,13 @@ def _eval_func( @_eval_types_impl.register def _eval_type_type(obj: type, ctx: EvalContext): if isinstance(obj, type) and issubclass(obj, typing.Generic): - ret = type( - obj.__name__, - (_EvalProxy,), - { - "__module__": obj.__module__, - "__name__": obj.__name__, - "__origin__": obj, - }, - ) - - # Need to add it to `seen` to handle recursion - ctx.seen[obj] = ret try: - ns = _apply_generic.apply(obj) + return _apply_generic.apply(obj, ctx) except Exception: - ctx.seen.pop(obj) + # XXX: should apply handle this? + ctx.seen.pop(obj, None) raise - for k, v in ns.items(): - setattr(ret, k, v) - - return ret - return obj From 2750daab2065cd42613fed6f02fd1d250658c43a Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 7 Jan 2026 16:04:37 -0800 Subject: [PATCH 2/7] Do a better job substituting into types --- tests/test_type_dir.py | 21 +++++++++++++++++++++ typemap/type_eval/_apply_generic.py | 25 ++++++++++++++++++------- typemap/type_eval/_eval_operators.py | 5 +---- typemap/type_eval/_eval_typing.py | 14 ++++++++++---- typemap/type_eval/_typing_inspect.py | 4 ++++ 5 files changed, 54 insertions(+), 15 deletions(-) diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index e10179f..7924b4c 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -286,6 +286,27 @@ def test_type_dir_7(): assert d is int +class Simple[T]: + simple: T + + +class Funny[T](Simple[list[T]]): + pass + + +class Funny2(Funny[int]): + pass + + +def test_type_dir_8(): + d = eval_typing(Funny2) + + assert format_helper.format_class(d) == textwrap.dedent("""\ + class Funny2: + simple: list[int] + """) + + def _get_member(members, name): return next( iter(m for m in members.__args__ if m.__args__[0].__args__[0] == name) diff --git a/typemap/type_eval/_apply_generic.py b/typemap/type_eval/_apply_generic.py index 86443c4..99490b0 100644 --- a/typemap/type_eval/_apply_generic.py +++ b/typemap/type_eval/_apply_generic.py @@ -4,7 +4,11 @@ import types import typing +from typing import _GenericAlias as typing_GenericAlias # type: ignore [attr-defined] # noqa: PLC2701 + + from . import _eval_typing +from . import _typing_inspect if typing.TYPE_CHECKING: from typing import Any @@ -44,8 +48,17 @@ def dump(self, *, _level: int = 0): b.dump(_level=_level + 1) +def substitute(ty, args): + if ty in args: + return args[ty] + elif isinstance(ty, (typing_GenericAlias, types.GenericAlias)): + return ty.__origin__[*[substitute(t, args) for t in ty.__args__]] + else: + return ty + + def box(cls: type[Any]) -> Boxed: - def _box(cls: type[Any], args: dict[str, Any]) -> Boxed: + def _box(cls: type[Any], args: dict[Any, Any]) -> Boxed: boxed_bases: list[Boxed] = [] orig_bases = cls.__dict__.get("__orig_bases__") @@ -60,14 +73,12 @@ def _box(cls: type[Any], args: dict[str, Any]) -> Boxed: if issubclass(base, typing.Generic): if base_params := getattr(base, "__parameters__", None): + # obase should be _GenericAlias... boxed_args = {} for param, arg in zip( base_params, obase.__args__, strict=True ): - if arg in args: - boxed_args[param] = args[arg] - else: - boxed_args[param] = arg + boxed_args[param] = substitute(arg, args) else: boxed_args = {} @@ -85,7 +96,7 @@ def _box(cls: type[Any], args: dict[str, Any]) -> Boxed: cls = cls.__origin__ else: if params := getattr(cls, "__parameters__", None): - args = {p: p for p in params} + args = {p: _typing_inspect.param_default(p) for p in params} else: args = {} @@ -120,7 +131,7 @@ def merge_boxed_mro(seqs: list[list[Boxed]]) -> list[Boxed]: def _compute_mro(C: Boxed) -> list[Boxed]: return merge_boxed_mro( - [[C]] + list(map(_compute_mro, C.bases)) + [list(C.bases)] + [[C]] + [_compute_mro(b) for b in C.bases] + [list(C.bases)] ) diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 23c29e6..f77b86f 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -423,10 +423,7 @@ def _get_defaults(base_head): return (typing.Any, SpecialFormEllipsis) if params := _get_params(base_head): - return tuple( - typing.Any if t.__default__ == typing.NoDefault else t.__default__ - for t in params - ) + return tuple(_typing_inspect.param_default(p) for p in params) return (typing.Any,) * arity diff --git a/typemap/type_eval/_eval_typing.py b/typemap/type_eval/_eval_typing.py index fa980e6..bab4cce 100644 --- a/typemap/type_eval/_eval_typing.py +++ b/typemap/type_eval/_eval_typing.py @@ -9,7 +9,7 @@ import types import typing -from typing import _GenericAlias # type: ignore [attr-defined] # noqa: PLC2701 +from typing import _GenericAlias as typing_GenericAlias # type: ignore [attr-defined] # noqa: PLC2701 if typing.TYPE_CHECKING: @@ -282,7 +282,12 @@ def _eval_type_alias(obj: typing.TypeAliasType, ctx: EvalContext): @_eval_types_impl.register -def _eval_types_generic(obj: types.GenericAlias, ctx: EvalContext): +def _eval_applied_type_alias(obj: types.GenericAlias, ctx: EvalContext): + """Eval a types.GenericAlias -- typically an applied type alias + + This is typically an application of a type alias... except it can + also be an application of a built-in type (like list, tuple, dict) + """ new_args = tuple(_eval_types(arg, ctx) for arg in obj.__args__) new_obj = obj.__origin__[new_args] # type: ignore[index] @@ -315,9 +320,10 @@ def _eval_types_generic(obj: types.GenericAlias, ctx: EvalContext): @_eval_types_impl.register -def _eval_typing_generic(obj: _GenericAlias, ctx: EvalContext): +def _eval_applied_class(obj: typing_GenericAlias, ctx: EvalContext): + """Eval a typing._GenericAlias -- an applied user-defined class""" # generic *classes* are typing._GenericAlias while generic type - # aliases are # types.GenericAlias? Why in the world. + # aliases are types.GenericAlias? Why in the world. if func := _eval_funcs.get(obj.__origin__): new_args = tuple(_eval_types(arg, ctx) for arg in obj.__args__) ret = func(*new_args, ctx=ctx) diff --git a/typemap/type_eval/_typing_inspect.py b/typemap/type_eval/_typing_inspect.py index ed3b82e..f6195ae 100644 --- a/typemap/type_eval/_typing_inspect.py +++ b/typemap/type_eval/_typing_inspect.py @@ -149,6 +149,10 @@ def is_eval_proxy(t: Any) -> TypeGuard[type[_eval_typing._EvalProxy]]: return isinstance(t, type) and issubclass(t, _eval_typing._EvalProxy) +def param_default(p) -> Any: + return Any if p.__default__ == typing.NoDefault else p.__default__ + + __all__ = ( "is_annotated", "is_forward_ref", From 4f2b90dc0a4de9224158f1500a1451d35a0bbd2a Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 7 Jan 2026 16:30:23 -0800 Subject: [PATCH 3/7] Tweak mro computation --- typemap/type_eval/_apply_generic.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/typemap/type_eval/_apply_generic.py b/typemap/type_eval/_apply_generic.py index 99490b0..2dc4f82 100644 --- a/typemap/type_eval/_apply_generic.py +++ b/typemap/type_eval/_apply_generic.py @@ -21,6 +21,7 @@ class Boxed: args: dict[Any, Any] str_args: dict[str, Any] = dataclasses.field(init=False) + mro: list[Boxed] = dataclasses.field(init=False) def __post_init__(self): object.__setattr__( @@ -28,6 +29,11 @@ def __post_init__(self): "str_args", {str(k): v for k, v in self.args.items()}, ) + object.__setattr__( + self, + "mro", + _compute_mro(self), + ) def __repr__(self): return f"Boxed<{self.cls} {self.args}>" @@ -90,6 +96,8 @@ def _box(cls: type[Any], args: dict[Any, Any]) -> Boxed: 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) ) @@ -103,10 +111,10 @@ def _box(cls: type[Any], args: dict[Any, Any]) -> Boxed: return _box(cls, args) -def merge_boxed_mro(seqs: list[list[Boxed]]) -> list[Boxed]: - res: list[Boxed] = [] +def merge_boxed_mro[T](seqs: list[list[T]]) -> list[T]: + res: list[T] = [] i = 0 - cand: Boxed | None = None + cand: T | None = None while 1: nonemptyseqs = [seq for seq in seqs if seq] if not nonemptyseqs: @@ -130,13 +138,7 @@ def merge_boxed_mro(seqs: list[list[Boxed]]) -> list[Boxed]: def _compute_mro(C: Boxed) -> list[Boxed]: - return merge_boxed_mro( - [[C]] + [_compute_mro(b) for b in C.bases] + [list(C.bases)] - ) - - -def compute_mro(C: type[Any]) -> list[Boxed]: - return _compute_mro(box(C)) + return merge_boxed_mro([[C]] + [b.mro for b in C.bases] + [list(C.bases)]) def make_func( @@ -173,7 +175,8 @@ def make_func( def apply( cls: type[Any], ctx: _eval_typing.EvalContext ) -> type[_eval_typing._EvalProxy]: - mro_boxed = compute_mro(cls) + cls_boxed = box(cls) + mro_boxed = cls_boxed.mro annos: dict[str, Any] = {} dct: dict[str, Any] = {} @@ -189,6 +192,9 @@ def apply( }, ) + # TODO: I think we want to create the whole mro chain... + # before we evaluate the contents? + # Run through the mro for boxed in reversed(mro_boxed): if af := getattr(boxed.cls, "__annotate__", None): From 7b7de3a61bcc6f0af9a74b9e966f9a5deed213a1 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 7 Jan 2026 16:34:36 -0800 Subject: [PATCH 4/7] Refactor apply some - factor out some code --- typemap/type_eval/_apply_generic.py | 145 +++++++++++++++------------- 1 file changed, 78 insertions(+), 67 deletions(-) diff --git a/typemap/type_eval/_apply_generic.py b/typemap/type_eval/_apply_generic.py index 2dc4f82..dd3741f 100644 --- a/typemap/type_eval/_apply_generic.py +++ b/typemap/type_eval/_apply_generic.py @@ -172,6 +172,81 @@ def make_func( return new_func +def _get_local_defns(boxed: Boxed) -> tuple[dict[str, Any], dict[str, Any]]: + annos: dict[str, Any] = {} + dct: dict[str, Any] = {} + + if af := getattr(boxed.cls, "__annotate__", None): + # Class has annotations, let's resolve generic arguments + + args = tuple( + types.CellType( + boxed.cls.__dict__ + if name == "__classdict__" + else boxed.str_args[name] + ) + for name in af.__code__.co_freevars + ) + + ff = types.FunctionType( + af.__code__, af.__globals__, af.__name__, None, args + ) + rr = ff(annotationlib.Format.VALUE) + + if rr: + for k, v in rr.items(): + if isinstance(v, str): + # Handle cases where annotation is explicitly a string, + # e.g.: + # + # class Foo[X]: + # x: "Foo[X | None]" + + annos[k] = eval(v, af.__globals__, boxed.str_args) + else: + annos[k] = v + elif af := getattr(boxed.cls, "__annotations__", None): + annos.update(af) + + for name, orig in boxed.cls.__dict__.items(): + if name in typing.EXCLUDED_ATTRIBUTES: # type: ignore[attr-defined] + continue + + stuff = inspect.unwrap(orig) + + if isinstance(stuff, types.FunctionType): + if af := getattr(stuff, "__annotate__", None): + params = dict( + zip( + map(str, stuff.__type_params__), + stuff.__type_params__, + strict=True, + ) + ) + + args = tuple( + types.CellType( + boxed.cls.__dict__ + if name == "__classdict__" + else params[name] + if name in params + else boxed.str_args[name] + ) + for name in af.__code__.co_freevars + ) + + ff = types.FunctionType( + af.__code__, af.__globals__, af.__name__, None, args + ) + rr = ff(annotationlib.Format.VALUE) + + dct[name] = make_func(orig, rr) + elif af := getattr(stuff, "__annotations__", None): + dct[name] = stuff + + return annos, dct + + def apply( cls: type[Any], ctx: _eval_typing.EvalContext ) -> type[_eval_typing._EvalProxy]: @@ -197,73 +272,9 @@ def apply( # Run through the mro for boxed in reversed(mro_boxed): - if af := getattr(boxed.cls, "__annotate__", None): - # Class has annotations, let's resolve generic arguments - - args = tuple( - types.CellType( - boxed.cls.__dict__ - if name == "__classdict__" - else boxed.str_args[name] - ) - for name in af.__code__.co_freevars - ) - - ff = types.FunctionType( - af.__code__, af.__globals__, af.__name__, None, args - ) - rr = ff(annotationlib.Format.VALUE) - - if rr: - for k, v in rr.items(): - if isinstance(v, str): - # Handle cases where annotation is explicitly a string, - # e.g.: - # - # class Foo[X]: - # x: "Foo[X | None]" - - annos[k] = eval(v, af.__globals__, boxed.str_args) - else: - annos[k] = v - elif af := getattr(boxed.cls, "__annotations__", None): - annos.update(af) - - for name, orig in boxed.cls.__dict__.items(): - if name in typing.EXCLUDED_ATTRIBUTES: # type: ignore[attr-defined] - continue - - stuff = inspect.unwrap(orig) - - if isinstance(stuff, types.FunctionType): - if af := getattr(stuff, "__annotate__", None): - params = dict( - zip( - map(str, stuff.__type_params__), - stuff.__type_params__, - strict=True, - ) - ) - - args = tuple( - types.CellType( - boxed.cls.__dict__ - if name == "__classdict__" - else params[name] - if name in params - else boxed.str_args[name] - ) - for name in af.__code__.co_freevars - ) - - ff = types.FunctionType( - af.__code__, af.__globals__, af.__name__, None, args - ) - rr = ff(annotationlib.Format.VALUE) - - dct[name] = make_func(orig, rr) - elif af := getattr(stuff, "__annotations__", None): - dct[name] = stuff + lannos, ldct = _get_local_defns(boxed) + annos.update(lannos) + dct.update(ldct) for k, v in annos.items(): annos[k] = _eval_typing._eval_types(v, ctx=ctx) From 042a20db5463bc25e5d1b494627e54aca890ea07 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 7 Jan 2026 17:03:36 -0800 Subject: [PATCH 5/7] Heavily refactor apply to create classes for the whole mro --- typemap/type_eval/_apply_generic.py | 79 ++++++++++++++++++---------- typemap/type_eval/_eval_operators.py | 6 +-- 2 files changed, 55 insertions(+), 30 deletions(-) diff --git a/typemap/type_eval/_apply_generic.py b/typemap/type_eval/_apply_generic.py index dd3741f..9847f29 100644 --- a/typemap/type_eval/_apply_generic.py +++ b/typemap/type_eval/_apply_generic.py @@ -35,6 +35,12 @@ def __post_init__(self): _compute_mro(self), ) + def alias_type(self): + if self.args: + return self.cls[*self.args.values()] + else: + return self.cls + def __repr__(self): return f"Boxed<{self.cls} {self.args}>" @@ -253,39 +259,58 @@ def apply( cls_boxed = box(cls) mro_boxed = cls_boxed.mro - annos: dict[str, Any] = {} - dct: dict[str, Any] = {} - - # We create it early so we can add it to seen, to handle recursion - ctx.seen[cls] = ret = type( - cls.__name__, - (_eval_typing._EvalProxy,), - { - "__module__": cls.__module__, - "__name__": cls.__name__, - "__origin__": cls, - }, - ) - # TODO: I think we want to create the whole mro chain... # before we evaluate the contents? - # Run through the mro + # FIXME: right now we flatten out all the attributes... but should we?? + + new = {} + + # Run through the mro and populate everything for boxed in reversed(mro_boxed): - lannos, ldct = _get_local_defns(boxed) - annos.update(lannos) - dct.update(ldct) + # We create it early so we can add it to seen, to handle recursion + # XXX: currently we are doing this even for types with no generics... + # that simplifies the flow... - probably keep it this way until + # we stop flattening attributes into every class + name = boxed.cls.__name__ + cboxed: Any + cboxed = type( + boxed.cls.__name__, + (_eval_typing._EvalProxy,), + { + "__module__": boxed.cls.__module__, + "__name__": name, + "__origin__": boxed.cls, + "__local_args__": tuple(boxed.args.values()), + }, + ) + ctx.seen[boxed.alias_type()] = new[boxed] = cboxed + + annos: dict[str, Any] = {} + dct: dict[str, Any] = {} + + cboxed.__local_annotations__, cboxed.__local_defns__ = _get_local_defns( + boxed + ) + for base in reversed(boxed.mro): + cbase = new[base] + annos.update(cbase.__local_annotations__) + dct.update(cbase.__local_defns__) # uh. - for k, v in annos.items(): - annos[k] = _eval_typing._eval_types(v, ctx=ctx) + cboxed.__defn_names__ = set(dct) + cboxed.__annotations__ = annos + cboxed.__generalized_mro__ = [new[b] for b in boxed.mro] - for k, v in dct.items(): - dct[k] = _eval_typing._eval_types(v, ctx=ctx) + for k, v in dct.items(): + setattr(cboxed, k, v) - dct["__annotations__"] = annos - dct["__generalized_mro__"] = mro_boxed + # Run through the mro again and evaluate everything + for cboxed in new.values(): + for k, v in cboxed.__annotations__.items(): + cboxed.__annotations__[k] = _eval_typing._eval_types(v, ctx=ctx) - for k, v in dct.items(): - setattr(ret, k, v) + for k in cboxed.__defn_names__: + v = cboxed.__dict__[k] + setattr(cboxed, k, _eval_typing._eval_types(v, ctx=ctx)) - return ret + return new[cls_boxed] diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index f77b86f..bedb41d 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -302,9 +302,9 @@ def _get_raw_args(tp, base_head, ctx) -> typing.Any: # Scan the fully-annotated MRO to find the base elif gen_mro := getattr(evaled, "__generalized_mro__", None): - for box in gen_mro: - if box.cls is base_head: - return tuple(box.args.values()) + for anc in gen_mro: + if _typing_inspect.get_head(anc) is base_head: + return anc.__local_args__ return None else: From e5c544abf01644d0ec726bad3592603c3bf7f21f Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Thu, 8 Jan 2026 12:07:48 -0800 Subject: [PATCH 6/7] Include args in apply type names --- typemap/type_eval/_apply_generic.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/typemap/type_eval/_apply_generic.py b/typemap/type_eval/_apply_generic.py index 9847f29..ec790c2 100644 --- a/typemap/type_eval/_apply_generic.py +++ b/typemap/type_eval/_apply_generic.py @@ -253,6 +253,16 @@ def _get_local_defns(boxed: Boxed) -> tuple[dict[str, Any], dict[str, Any]]: return annos, dct +def _type_repr(t: Any) -> str: + if isinstance(t, type): + if t.__module__ == "builtins": + return t.__qualname__ + else: + return f"{t.__module__}.{t.__qualname__}" + else: + return repr(t) + + def apply( cls: type[Any], ctx: _eval_typing.EvalContext ) -> type[_eval_typing._EvalProxy]: @@ -274,14 +284,18 @@ def apply( # we stop flattening attributes into every class name = boxed.cls.__name__ cboxed: Any + + args = tuple(boxed.args.values()) + args_str = ", ".join(_type_repr(a) for a in args) + fullname = f"{name}[{args_str}]" if args_str else name cboxed = type( - boxed.cls.__name__, + fullname, (_eval_typing._EvalProxy,), { "__module__": boxed.cls.__module__, - "__name__": name, + "__name__": fullname, "__origin__": boxed.cls, - "__local_args__": tuple(boxed.args.values()), + "__local_args__": args, }, ) ctx.seen[boxed.alias_type()] = new[boxed] = cboxed From 0b20860afc7159ff89dd1a8c98c1c97f912a34c2 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Thu, 8 Jan 2026 12:08:26 -0800 Subject: [PATCH 7/7] Properly populate the origin type for members --- tests/test_type_dir.py | 20 +++----------------- typemap/type_eval/_apply_generic.py | 6 ++++++ typemap/type_eval/_eval_operators.py | 11 ++++++++--- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 7924b4c..30c345d 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -2,7 +2,6 @@ import typing from typing import Literal, Never, TypeVar, Union -import pytest from typemap.type_eval import eval_typing from typemap.typing import ( @@ -313,9 +312,6 @@ def _get_member(members, name): ) -@pytest.mark.xfail( - reason="Members does not actually report the correct origin class!" -) def test_type_members_attr_(): d = eval_typing(Members[Final]) member = _get_member(d, "ordinary") @@ -328,7 +324,7 @@ def test_type_members_func_1a(): d = eval_typing(Members[Final]) member = _get_member(d, "foo") assert typing.get_origin(member) is Member - name, typ, quals, _origin = typing.get_args(member) + name, typ, quals, origin = typing.get_args(member) assert name == typing.Literal["foo"] assert quals == typing.Literal["ClassVar"] @@ -336,23 +332,13 @@ def test_type_members_func_1a(): str(typ) == "\ typing.Callable[[\ -typemap.typing.Param[typing.Literal['self'], tests.test_type_dir.Final, typing.Never], \ +typemap.typing.Param[typing.Literal['self'], tests.test_type_dir.Base[int], typing.Never], \ typemap.typing.Param[typing.Literal['a'], int | None, typing.Never], \ typemap.typing.Param[typing.Literal['b'], int, typing.Literal['keyword', \ 'default']]], \ dict[str, int]]" ) - -@pytest.mark.xfail( - reason="Members does not actually report the correct origin class!" -) -def test_type_members_func_1b(): - # This should be merged up with 1a once it is fixed - d = eval_typing(Members[Final]) - member = _get_member(d, "foo") - assert typing.get_origin(member) is Member - _, _, _, origin = typing.get_args(member) assert origin.__name__ == "Base[int]" @@ -367,7 +353,7 @@ def test_type_members_func_2(): assert ( str(typ) == "\ -classmethod[tests.test_type_dir.Final, tuple[typemap.typing.Param[typing.Literal['a'], int | None, typing.Never], typemap.typing.Param[typing.Literal['b'], ~K, typing.Never]], dict[str, int]]" +classmethod[tests.test_type_dir.Base[int], tuple[typemap.typing.Param[typing.Literal['a'], int | None, typing.Never], typemap.typing.Param[typing.Literal['b'], ~K, typing.Never]], dict[str, int]]" ) diff --git a/typemap/type_eval/_apply_generic.py b/typemap/type_eval/_apply_generic.py index ec790c2..fa13d26 100644 --- a/typemap/type_eval/_apply_generic.py +++ b/typemap/type_eval/_apply_generic.py @@ -273,6 +273,8 @@ def apply( # before we evaluate the contents? # FIXME: right now we flatten out all the attributes... but should we?? + # XXX: Yeah, a lot of work is put into copying everything into every + # class and it is not worth it, at all. new = {} @@ -302,6 +304,7 @@ def apply( annos: dict[str, Any] = {} dct: dict[str, Any] = {} + sources: dict[str, Any] = {} cboxed.__local_annotations__, cboxed.__local_defns__ = _get_local_defns( boxed @@ -310,9 +313,12 @@ def apply( cbase = new[base] annos.update(cbase.__local_annotations__) dct.update(cbase.__local_defns__) # uh. + for k in [*cbase.__local_annotations__, *cbase.__local_defns__]: + sources[k] = cbase cboxed.__defn_names__ = set(dct) cboxed.__annotations__ = annos + cboxed.__defn_sources__ = sources cboxed.__generalized_mro__ = [new[b] for b in boxed.mro] for k, v in dct.items(): diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index bedb41d..65ba807 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -54,6 +54,9 @@ def get_annotated_type_hints(cls, **kwargs): for acls in cls.__mro__: if not hasattr(acls, "__annotations__"): continue + # XXX: This is super janky; we should just use the real mro + # and not flatten things + sources = getattr(acls, "__defn_sources__", {}) for k in acls.__annotations__: if k not in hints: quals = set() @@ -73,7 +76,7 @@ def get_annotated_type_hints(cls, **kwargs): else: break - hints[k] = ty, tuple(sorted(quals)), acls + hints[k] = ty, tuple(sorted(quals)), sources.get(k, acls) # Stop early if we are done. if len(hints) == len(ohints): @@ -84,6 +87,7 @@ def get_annotated_type_hints(cls, **kwargs): def get_annotated_method_hints(tp): hints = {} for ptp in reversed(tp.mro()): + sources = getattr(ptp, "__defn_sources__", {}) for name, attr in ptp.__dict__.items(): if isinstance( attr, @@ -97,10 +101,11 @@ def get_annotated_method_hints(tp): if attr is typing._no_init_or_replace_init: continue + rtp = sources.get(name, ptp) hints[name] = ( - _function_type(attr, receiver_type=ptp), + _function_type(attr, receiver_type=rtp), ("ClassVar",), - ptp, + rtp, ) return hints