From cdc4938e854d068e4dc91b5b635cd9fecb2b410f Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 8 Oct 2025 13:02:38 -0700 Subject: [PATCH 1/3] Use the calling frame's name to produce a name for the NewProtocol --- tests/test_call.py | 3 ++- tests/test_type_dir.py | 5 +++-- tests/test_type_eval.py | 4 ++-- typemap/typing.py | 19 +++++++++++++++++-- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/tests/test_call.py b/tests/test_call.py index 2acf3ff..2b40129 100644 --- a/tests/test_call.py +++ b/tests/test_call.py @@ -17,8 +17,9 @@ def test_call_1(): ret = eval_call(func, a=1, b=2, c="aaa") fmt = format_helper.format_class(ret) + # XXX: Do we like this name? assert fmt == textwrap.dedent("""\ - class Protocol: + class __annotate__: a: int b: int c: int diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 9dc9c5f..d010906 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -81,11 +81,12 @@ def sbase[Z](cls, a: int | Literal['gotcha!'] | Z | None, b: ~K) -> dict[str, in def test_type_dir_2(): d = eval_typing(OptionalFinal) - # XXX: the class should probably be named something like "AllOptional__T" + # XXX: should the class name be further mangled to something like + # "AllOptional__T"? # XXX: `DirProperties` skips methods, true to its name. Perhaps we just need # `Dir` that would iterate over everything assert format_helper.format_class(d) == textwrap.dedent("""\ - class Protocol: + class AllOptional: last: int | typing.Literal[True] | None iii: str | int | typing.Literal['gotcha!'] | None t: dict[str, str | int | typing.Literal['gotcha!']] | None diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 8882cfc..9849c2c 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -52,11 +52,11 @@ def test_eval_types_2(): assert evaled.__annotations__["a"].__args__[0] is evaled assert format_helper.format_class(evaled) == textwrap.dedent("""\ - class Protocol: + class MapRecursive: n: int | typing.Literal['gotcha!'] m: str | typing.Literal['gotcha!'] t: typing.Literal[False] | typing.Literal['gotcha!'] - a: abc.Protocol | typing.Literal['gotcha!'] + a: tests.test_type_eval.MapRecursive | typing.Literal['gotcha!'] fff: int | typing.Literal['gotcha!'] control: float """) diff --git a/typemap/typing.py b/typemap/typing.py index 104e63a..ebc0c39 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -88,9 +88,24 @@ def __getitem__(cls, val: list[Property]): dct = {} dct["__annotations__"] = {prop.name: prop.type for prop in val} + frame = inspect.currentframe() + + module_name = __name__ + name = "Protocol" + + # Peak at the calling frame and use that to produce + # a better name than Protocol. + # This is probably a 3.14 special? + if frame and (prev := frame.f_back): + name = prev.f_code.co_name + module_name = inspect.getmodule(prev.f_code).__name__ + # qualname?? + + dct["__module__"] = module_name + mcls = type(typing.Protocol) - # TODO: Replace the "Protocol" name with the type alias name - return mcls("Protocol", (typing.Protocol,), dct) + cls = mcls(name, (typing.Protocol,), dct) + return cls class NewProtocol(metaclass=NewProtocolMeta): From decc3d665b11950df861ed473130e694c0dfa99b Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 8 Oct 2025 13:32:44 -0700 Subject: [PATCH 2/3] Try sticking it in the context instead --- tests/test_call.py | 4 ++-- tests/test_type_dir.py | 4 +--- tests/test_type_eval.py | 4 ++-- typemap/type_eval/__init__.py | 4 ++-- typemap/type_eval/_eval_typing.py | 15 +++++++++++---- typemap/typing.py | 18 +++++++----------- 6 files changed, 25 insertions(+), 24 deletions(-) diff --git a/tests/test_call.py b/tests/test_call.py index 2b40129..a0e92e0 100644 --- a/tests/test_call.py +++ b/tests/test_call.py @@ -17,9 +17,9 @@ def test_call_1(): ret = eval_call(func, a=1, b=2, c="aaa") fmt = format_helper.format_class(ret) - # XXX: Do we like this name? + # XXX: We want better than this for a name assert fmt == textwrap.dedent("""\ - class __annotate__: + class NewProtocol: a: int b: int c: int diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index d010906..98842a4 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -81,12 +81,10 @@ def sbase[Z](cls, a: int | Literal['gotcha!'] | Z | None, b: ~K) -> dict[str, in def test_type_dir_2(): d = eval_typing(OptionalFinal) - # XXX: should the class name be further mangled to something like - # "AllOptional__T"? # XXX: `DirProperties` skips methods, true to its name. Perhaps we just need # `Dir` that would iterate over everything assert format_helper.format_class(d) == textwrap.dedent("""\ - class AllOptional: + class AllOptional[tests.test_type_dir.Final]: last: int | typing.Literal[True] | None iii: str | int | typing.Literal['gotcha!'] | None t: dict[str, str | int | typing.Literal['gotcha!']] | None diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 9849c2c..26b4a45 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -52,11 +52,11 @@ def test_eval_types_2(): assert evaled.__annotations__["a"].__args__[0] is evaled assert format_helper.format_class(evaled) == textwrap.dedent("""\ - class MapRecursive: + class MapRecursive[tests.test_type_eval.Recursive]: n: int | typing.Literal['gotcha!'] m: str | typing.Literal['gotcha!'] t: typing.Literal[False] | typing.Literal['gotcha!'] - a: tests.test_type_eval.MapRecursive | typing.Literal['gotcha!'] + a: tests.test_type_eval.MapRecursive[tests.test_type_eval.Recursive] | typing.Literal['gotcha!'] fff: int | typing.Literal['gotcha!'] control: float """) diff --git a/typemap/type_eval/__init__.py b/typemap/type_eval/__init__.py index a7738e4..dcba911 100644 --- a/typemap/type_eval/__init__.py +++ b/typemap/type_eval/__init__.py @@ -1,6 +1,6 @@ from ._eval_call import eval_call -from ._eval_typing import eval_typing +from ._eval_typing import eval_typing, _current_context -__all__ = ("eval_typing", "eval_call") +__all__ = ("eval_typing", "eval_call", "_current_context") 1 diff --git a/typemap/type_eval/_eval_typing.py b/typemap/type_eval/_eval_typing.py index 720dae5..b31d624 100644 --- a/typemap/type_eval/_eval_typing.py +++ b/typemap/type_eval/_eval_typing.py @@ -21,6 +21,7 @@ @dataclasses.dataclass class EvalContext: seen: dict[Any, Any] + current_alias: types.GenericAlias | None = None # `eval_types()` calls can be nested, context must be preserved @@ -131,15 +132,21 @@ def _eval_generic(obj: types.GenericAlias, ctx: EvalContext): args = tuple(types.CellType(_eval_types(arg, ctx)) for arg in obj.__args__) mod = sys.modules[obj.__module__] - ff = types.FunctionType(func.__code__, mod.__dict__, None, None, args) - unpacked = ff(annotationlib.Format.VALUE) - ctx.seen[obj] = unpacked + old_obj = ctx.current_alias + ctx.current_alias = obj + try: + ff = types.FunctionType(func.__code__, mod.__dict__, None, None, args) + unpacked = ff(annotationlib.Format.VALUE) + + ctx.seen[obj] = unpacked evaled = _eval_types(unpacked, ctx) except Exception: - ctx.seen.pop(obj) + ctx.seen.pop(obj, None) raise + finally: + ctx.current_alias = old_obj return evaled diff --git a/typemap/typing.py b/typemap/typing.py index ebc0c39..86dd658 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -88,18 +88,14 @@ def __getitem__(cls, val: list[Property]): dct = {} dct["__annotations__"] = {prop.name: prop.type for prop in val} - frame = inspect.currentframe() - module_name = __name__ - name = "Protocol" - - # Peak at the calling frame and use that to produce - # a better name than Protocol. - # This is probably a 3.14 special? - if frame and (prev := frame.f_back): - name = prev.f_code.co_name - module_name = inspect.getmodule(prev.f_code).__name__ - # qualname?? + name = "NewProtocol" + + # If the type evaluation context + ctx = type_eval._current_context.get() + if ctx and ctx.current_alias: + name = str(ctx.current_alias) + module_name = ctx.current_alias.__module__ dct["__module__"] = module_name From d289778f2e1388507b544b6d632f2f3d795bc457 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 8 Oct 2025 14:42:28 -0700 Subject: [PATCH 3/3] add _ensure_context, use it in eval_call --- typemap/type_eval/__init__.py | 4 ++-- typemap/type_eval/_eval_call.py | 5 +++++ typemap/type_eval/_eval_typing.py | 20 ++++++++++++++++++-- typemap/typing.py | 4 ++-- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/typemap/type_eval/__init__.py b/typemap/type_eval/__init__.py index dcba911..80690a4 100644 --- a/typemap/type_eval/__init__.py +++ b/typemap/type_eval/__init__.py @@ -1,6 +1,6 @@ from ._eval_call import eval_call -from ._eval_typing import eval_typing, _current_context +from ._eval_typing import eval_typing, _get_current_context -__all__ = ("eval_typing", "eval_call", "_current_context") +__all__ = ("eval_typing", "eval_call", "_get_current_context") 1 diff --git a/typemap/type_eval/_eval_call.py b/typemap/type_eval/_eval_call.py index 93f7e50..aff404a 100644 --- a/typemap/type_eval/_eval_call.py +++ b/typemap/type_eval/_eval_call.py @@ -11,6 +11,11 @@ def eval_call(func: types.FunctionType, /, *args: Any, **kwargs: Any) -> Any: + with _eval_typing._ensure_context(): + return _eval_call(func, *args, **kwargs) + + +def _eval_call(func: types.FunctionType, /, *args: Any, **kwargs: Any) -> Any: vars = {} params = func.__type_params__ diff --git a/typemap/type_eval/_eval_typing.py b/typemap/type_eval/_eval_typing.py index b31d624..e1fbdaa 100644 --- a/typemap/type_eval/_eval_typing.py +++ b/typemap/type_eval/_eval_typing.py @@ -1,5 +1,6 @@ import annotationlib +import contextlib import contextvars import dataclasses import functools @@ -30,7 +31,8 @@ class EvalContext: ) -def eval_typing(obj: typing.Any): +@contextlib.contextmanager +def _ensure_context() -> typing.Iterator[EvalContext]: ctx = _current_context.get() ctx_set = False if ctx is None: @@ -41,12 +43,26 @@ def eval_typing(obj: typing.Any): ctx_set = True try: - return _eval_types(obj, ctx) + yield ctx finally: if ctx_set: _current_context.set(None) +def _get_current_context() -> EvalContext: + ctx = _current_context.get() + if not ctx: + raise RuntimeError( + "type_eval._get_current_context() called outside of eval_types()" + ) + return ctx + + +def eval_typing(obj: typing.Any): + with _ensure_context() as ctx: + return _eval_types(obj, ctx) + + def _eval_types(obj: typing.Any, ctx: EvalContext): if obj in ctx.seen: return ctx.seen[obj] diff --git a/typemap/typing.py b/typemap/typing.py index 86dd658..ad31ddf 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -92,8 +92,8 @@ def __getitem__(cls, val: list[Property]): name = "NewProtocol" # If the type evaluation context - ctx = type_eval._current_context.get() - if ctx and ctx.current_alias: + ctx = type_eval._get_current_context() + if ctx.current_alias: name = str(ctx.current_alias) module_name = ctx.current_alias.__module__