From 9892445968f0c3a0012178cecc6025cba1aaf106 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Mon, 12 Jan 2026 15:35:53 -0800 Subject: [PATCH 1/3] Move special forms to new file. --- typemap/type_eval/_eval_typing.py | 9 +++------ typemap/type_eval/_special_form.py | 30 +++++++++++++++++++++++++++++ typemap/typing.py | 31 +++++------------------------- 3 files changed, 38 insertions(+), 32 deletions(-) create mode 100644 typemap/type_eval/_special_form.py diff --git a/typemap/type_eval/_eval_typing.py b/typemap/type_eval/_eval_typing.py index 7f76660..936e29d 100644 --- a/typemap/type_eval/_eval_typing.py +++ b/typemap/type_eval/_eval_typing.py @@ -19,6 +19,7 @@ from typing import Any from . import _apply_generic +from ._special_form import _special_form_evaluator __all__ = ("eval_typing",) @@ -113,24 +114,20 @@ class EvalContext: @contextlib.contextmanager def _ensure_context() -> typing.Iterator[EvalContext]: - import typemap.typing as nt - ctx = _current_context.get() ctx_set = False if ctx is None: ctx = EvalContext() _current_context.set(ctx) ctx_set = True - evaluator_token = nt.special_form_evaluator.set( - lambda t: _eval_types(t, ctx) - ) + evaluator_token = _special_form_evaluator.set(lambda t: _eval_types(t, ctx)) try: yield ctx finally: if ctx_set: _current_context.set(None) - nt.special_form_evaluator.reset(evaluator_token) + _special_form_evaluator.reset(evaluator_token) def _get_current_context() -> EvalContext: diff --git a/typemap/type_eval/_special_form.py b/typemap/type_eval/_special_form.py new file mode 100644 index 0000000..a1ca70a --- /dev/null +++ b/typemap/type_eval/_special_form.py @@ -0,0 +1,30 @@ +import contextvars +import typing +from typing import _GenericAlias # type: ignore + + +_SpecialForm: typing.Any = typing._SpecialForm + + +# TODO: type better +_special_form_evaluator: contextvars.ContextVar[ + typing.Callable[[typing.Any], typing.Any] | None +] = contextvars.ContextVar("special_form_evaluator", default=None) + + +class _IterGenericAlias(_GenericAlias, _root=True): # type: ignore[call-arg] + def __iter__(self): + evaluator = _special_form_evaluator.get() + if evaluator: + return evaluator(self) + else: + return iter(typing.TypeVarTuple("_IterDummy")) + + +class _IsGenericAlias(_GenericAlias, _root=True): # type: ignore[call-arg] + def __bool__(self): + evaluator = _special_form_evaluator.get() + if evaluator: + return evaluator(self) + else: + return False diff --git a/typemap/typing.py b/typemap/typing.py index e972ad4..928d46f 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -1,8 +1,10 @@ -import contextvars import typing -from typing import _GenericAlias # type: ignore -_SpecialForm: typing.Any = typing._SpecialForm +from .type_eval._special_form import ( + _IterGenericAlias, + _IsGenericAlias, + _SpecialForm, +) # Not type-level computation but related @@ -113,35 +115,12 @@ class NewProtocol[*T]: ################################################################## -# TODO: type better -special_form_evaluator: contextvars.ContextVar[ - typing.Callable[[typing.Any], typing.Any] | None -] = contextvars.ContextVar("special_form_evaluator", default=None) - - -class _IterGenericAlias(_GenericAlias, _root=True): # type: ignore[call-arg] - def __iter__(self): - evaluator = special_form_evaluator.get() - if evaluator: - return evaluator(self) - else: - return iter(typing.TypeVarTuple("_IterDummy")) - @_SpecialForm def Iter(self, tp): return _IterGenericAlias(self, (tp,)) -class _IsGenericAlias(_GenericAlias, _root=True): # type: ignore[call-arg] - def __bool__(self): - evaluator = special_form_evaluator.get() - if evaluator: - return evaluator(self) - else: - return False - - @_SpecialForm def IsSubtype(self, tps): return _IsGenericAlias(self, tps) From 7a0df62e5eda9bbc96d9d3da7363c824cb7ec724 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Mon, 12 Jan 2026 15:43:23 -0800 Subject: [PATCH 2/3] Add test. --- tests/test_type_eval.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index dcb0168..16445b8 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -769,3 +769,26 @@ def test_callable_to_signature(): '(_arg0: int, /, b: int, c: int = ..., *args: int, ' 'd: int, e: int = ..., **kwargs: int) -> int' ) + + +type IsNotInt[T] = not Is[T, int] +type IsNotStr[T] = not Is[T, str] +type IsNotIntOrStr[T] = IsNotInt[T] and IsNotStr[T] + + +type SetOfNotInt[T] = set[T] if IsNotInt[T] else T +type SetOfNotIntOrStr[T] = set[T] if IsNotIntOrStr[T] else T + + +def test_eval_if_generic_01(): + t = eval_typing(SetOfNotInt[int]) + assert t is int + t = eval_typing(SetOfNotInt[str]) + assert t == set[str] + + t = eval_typing(SetOfNotIntOrStr[int]) + assert t is int + t = eval_typing(SetOfNotIntOrStr[str]) + assert t is str + t = eval_typing(SetOfNotIntOrStr[float]) + assert t == set[float] From 4cbf96d052d2b96a0be40d13387b7c2c077ead58 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Mon, 12 Jan 2026 15:45:09 -0800 Subject: [PATCH 3/3] Add bool_special_form. --- tests/test_type_eval.py | 17 +++++-- typemap/type_eval/_eval_typing.py | 49 +++++++++++++++++++- typemap/type_eval/_special_form.py | 74 +++++++++++++++++++++++++++++- typemap/typing.py | 5 ++ 4 files changed, 140 insertions(+), 5 deletions(-) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 16445b8..4c58f06 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -33,6 +33,7 @@ StrConcat, StrSlice, Uppercase, + bool_special_form, ) from . import format_helper @@ -771,9 +772,19 @@ def test_callable_to_signature(): ) -type IsNotInt[T] = not Is[T, int] -type IsNotStr[T] = not Is[T, str] -type IsNotIntOrStr[T] = IsNotInt[T] and IsNotStr[T] +@bool_special_form +class IsNotInt[T]: + __expr__ = not Is[T, int] + + +@bool_special_form +class IsNotStr[T]: + __expr__ = not Is[T, str] + + +@bool_special_form +class IsNotIntOrStr[T]: + __expr__ = IsNotInt[T] and IsNotStr[T] type SetOfNotInt[T] = set[T] if IsNotInt[T] else T diff --git a/typemap/type_eval/_eval_typing.py b/typemap/type_eval/_eval_typing.py index 936e29d..476f332 100644 --- a/typemap/type_eval/_eval_typing.py +++ b/typemap/type_eval/_eval_typing.py @@ -19,7 +19,11 @@ from typing import Any from . import _apply_generic -from ._special_form import _special_form_evaluator +from ._special_form import ( + BoolSpecialMetadata, + _bool_special_form_registry, + _special_form_evaluator, +) __all__ = ("eval_typing",) @@ -346,6 +350,46 @@ def _eval_applied_type_alias(obj: types.GenericAlias, ctx: EvalContext): return evaled +def _eval_bool_special_form( + metadata: BoolSpecialMetadata, + new_args: tuple[typing.Any, ...], + ctx: EvalContext, +) -> bool: + import ast + + original_cls = metadata.cls + + try: + namespace = {} + + # Add the class's module + if cls_module := sys.modules.get(original_cls.__module__): + namespace.update(cls_module.__dict__) + + # Add type parameters with their substituted values + type_params = metadata.type_params + if type_params and new_args: + for param, arg in zip(type_params, new_args, strict=False): + namespace[param.__name__] = arg + + expr = compile( + ast.Expression(body=metadata.expr_node), # type: ignore[arg-type] + '', + 'eval', + ) + bool_expr = eval(expr, namespace) + + # Evaluate the type expression + result = _eval_types(bool_expr, ctx) + + return result + + except Exception as e: + raise RuntimeError( + f"Failed to evaluate special form for {original_cls.__name__}: {e}" + ) from e + + @_eval_types_impl.register def _eval_applied_class(obj: typing_GenericAlias, ctx: EvalContext): """Eval a typing._GenericAlias -- an applied user-defined class""" @@ -353,6 +397,9 @@ def _eval_applied_class(obj: typing_GenericAlias, ctx: EvalContext): # aliases are types.GenericAlias? Why in the world. new_args = tuple(_eval_types(arg, ctx) for arg in typing.get_args(obj)) + if metadata := _bool_special_form_registry.get(obj.__origin__): + return _eval_bool_special_form(metadata, new_args, ctx) + if func := _eval_funcs.get(obj.__origin__): ret = func(*new_args, ctx=ctx) # return _eval_types(ret, ctx) # ??? diff --git a/typemap/type_eval/_special_form.py b/typemap/type_eval/_special_form.py index a1ca70a..dcc5bae 100644 --- a/typemap/type_eval/_special_form.py +++ b/typemap/type_eval/_special_form.py @@ -1,4 +1,6 @@ +import ast import contextvars +import dataclasses import typing from typing import _GenericAlias # type: ignore @@ -21,10 +23,80 @@ def __iter__(self): return iter(typing.TypeVarTuple("_IterDummy")) -class _IsGenericAlias(_GenericAlias, _root=True): # type: ignore[call-arg] +class _BoolGenericAlias(_GenericAlias, _root=True): # type: ignore[call-arg] def __bool__(self): evaluator = _special_form_evaluator.get() if evaluator: return evaluator(self) else: return False + + +_IsGenericAlias = _BoolGenericAlias + + +_bool_special_form_registry: dict[typing.Any, BoolSpecialMetadata] = {} + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class BoolSpecialMetadata: + cls: type + type_params: tuple[type] + expr_node: ast.AST + + +def _register_bool_special_form(cls): + import inspect + import textwrap + + type_params = getattr(cls, '__type_params__', ()) + + if '__expr__' not in cls.__dict__: + raise TypeError(f"{cls.__name__} must have an '__expr__' field") + + # Parse __expr__ to get the assigned expression + source = inspect.getsource(cls) + source = textwrap.dedent(source) + tree = ast.parse(source) + + expr_node = None + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef): + for item in node.body: + if isinstance(item, ast.AnnAssign): + # __expr__: SomeType = expression + if ( + isinstance(item.target, ast.Name) + and item.target.id == '__expr__' + ): + expr_node = item.value + break + elif isinstance(item, ast.Assign): + # __expr__ = expression + for target in item.targets: + if ( + isinstance(target, ast.Name) + and target.id == '__expr__' + ): + expr_node = item.value + break + if expr_node: + break + if expr_node: + break + + if expr_node is None: + raise TypeError(f"Could not find __expr__ assignment in {cls.__name__}") + + def impl_func(self, params): + return _BoolGenericAlias(self, params) + + sf = _SpecialForm(impl_func) + + _bool_special_form_registry[sf] = BoolSpecialMetadata( + cls=cls, + type_params=type_params, + expr_node=expr_node, + ) + + return sf diff --git a/typemap/typing.py b/typemap/typing.py index 928d46f..6852ed2 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -4,6 +4,7 @@ _IterGenericAlias, _IsGenericAlias, _SpecialForm, + _register_bool_special_form, ) # Not type-level computation but related @@ -132,3 +133,7 @@ def IsSubSimilar(self, tps): Is = IsSubSimilar + + +def bool_special_form(cls): + return _register_bool_special_form(cls)